Go Conference'19 Summer in Fukuoka
JavaプログラムをGoに移植するためのテクニック――継承と例外Go Conference'19 Summer in Fukuoka牧内大輔 @makki_d
View Slide
自己紹介● 牧内大輔● MakKi / @makki_d● 所属:KLab株式会社○ エンジニアリングマネージャー○ オンライン対戦の中継サーバを Goで書いたり○ PHP/Python/C#その他必要とあればなんでも● gozxing○ https://github.com/makiuchi-d/gozxing○ 1D/2Dバーコードリーダーライブラリ○ JavaのZXingをGoに移植
会社紹介KLab(くらぶ)株式会社● 今日は休日出勤扱い(旅費・宿泊費も会社もち)● モバイルオンラインゲームの開発運用● 福岡オフィスあります(KBCビル)● KLab福岡Meetup Twitter:@KLab_KFMGo言語の活用事例● オンライン対戦同期サーバ● MMORPGのゲームサーバ● インフラ管理系ツール● その他細かいツール類やslackbotなど
なぜJavaをGoに移植するのか
ソフトウェア資産を活かしたい● Goは比較的新しい言語○ 2009年11月10日に発表● JavaやC++などのソフトウェア資産をGoでも利用したい○ Goの標準ライブラリはかなり充実している○ その範疇外のライブラリも世の中にはたくさんある● PureなGoで書かれている価値○ クロスプラットフォーム対応のしやすさなど● Go向けに設計し直すのは大変○ 言語構造、思想の違いが大きい○ できることなら設計や構造をそのまま利用したい
今日のお話● Java製1D/2Dバーコードライブラリ「ZXing」をGoに移植しました○ https://github.com/makiuchi-d/gozxing● 元のJavaコードとGoのコードを見比べながら実際に使っているテクニックをいくつか紹介します○ クラスと継承■ メソッドオーバーライド○ 例外■ 例外型の階層構造■ 例外種別判定
諸注意この発表は、Javaの構造をそのままGoに移植するためのテクニックの紹介です。元コードの構造をそのまま残すため、Goでは推奨されない書き方が出てきます。コード断片を表示しますが、見にくかったらごめんなさい。Javaを題材としていますが、C++やC#のように同様のクラスシステムと例外機構を持つ言語にも適用できると思います。もっと良い書き方があれば教えてください。
クラスと継承
Goに移植するときのポイント● 構造体とレシーバ関数でクラスを表現● 構造体埋め込みで継承っぽい動作● Goではメソッドオーバーライドができない
メソッドオーバーライドできない● レシーバの型は元の型のまま○ 埋め込まれた型のレシーバが呼ばれる○ 親クラスからは自身のメソッドになるBarkTwiceの中のdogは*Dogなので*DogのBarkが呼ばれるC++で言う仮想関数テーブルが必要type Dog struct {}func (dog *Dog) Bark() {fmt.Print("ワン")}func (dog *Dog) BarkTwice() {dog.Bark()dog.Bark()}type AmericanDog struct {*Dog}func (dog *AmericanDog) Bark() {fmt.Print("Bow");}dog := &AmericanDog{&Dog{}}dog.BarkTwice()
Java: OneDReader, Code128Readerの実装public abstract class OneDReader implements Reader {@Overridepublic Result decode(BinaryBitmap image, 略) throws NotFoundException, FormatException {...Result result = decodeRow(rowNumber, row, hints);...}public abstract Result decodeRow(int rowNumber, BitArray row, Map hints)throws NotFoundException, ChecksumException, FormatException;}public final class Code128Reader extends OneDReader {@Overridepublic Result decodeRow(int rowNumber, BitArray row, Map hints)throws NotFoundException, ChecksumException, FormatException {...}}
Java: OneDReader, Code128Readerの実装public abstract class OneDReader implements Reader {@Overridepublic Result decode(BinaryBitmap image, 略) throws NotFoundException, FormatException {...Result result = decodeRow(rowNumber, row, hints);...}public abstract Result decodeRow(int rowNumber, BitArray row, Map hints)throws NotFoundException, ChecksumException, FormatException;}public final class Code128Reader extends OneDReader {@Overridepublic Result decodeRow(int rowNumber, BitArray row, Map hints)throws NotFoundException, ChecksumException, FormatException {...}}Readerインターフェイスの実装派生クラスのdecodeRow()呼び出し派生クラスでdeocdeRowの実装を要求派生クラスでのdecodeRowの実装派生クラス
Go: OneDReader, Code128Readerの実装type RowDecoder interface {DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error)}type OneDReader struct {RowDecoder}func NewOneDReader(rowDecoder RowDecoder) *OneDReader {return &OneDReader{rowDecoder}}func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) {...result, e := this.DecodeRow(rowNumber, row, hints)...}
Go: OneDReader, Code128Readerの実装type RowDecoder interface {DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error)}type OneDReader struct {RowDecoder}func NewOneDReader(rowDecoder RowDecoder) *OneDReader {return &OneDReader{rowDecoder}}func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) {...result, e := this.DecodeRow(rowNumber, row, hints)...}Readerインターフェイスの実装埋め込んだRowDecoderのDecodeRowの呼び出しオーバーライドするメソッドを集めたインターフェイス(仮想関数テーブル)仮想関数テーブルの埋め込み初期化関数で実装を要求
Go: OneDReader, Code128Readerの実装type RowDecoder interface {DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error)}type OneDReader struct {RowDecoder}func NewOneDReader(rowDecoder RowDecoder) *OneDReader {return &OneDReader{rowDecoder}}func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) {...result, e := this.DecodeRow(rowNumber, row, hints)...}Readerインターフェイスの実装埋め込んだRowDecoderのDecodeRowの呼び出しオーバーライドするメソッドを集めたインターフェイス(仮想関数テーブル)仮想関数テーブルの埋め込み初期化関数で実装を要求type code128Reader struct {*OneDReader}func NewCode128Reader() gozxing.Reader {this := &code128Reader{}this.OneDReader = NewOneDReader(this)return this}func (*code128Reader) DecodeRow(略) (*gozxing.Result, error) {...}
Go: OneDReader, Code128Readerの実装type RowDecoder interface {DecodeRow(rowNumber int, row *gozxing.BitArray, 略) (*gozxing.Result, error)}type OneDReader struct {RowDecoder}func NewOneDReader(rowDecoder RowDecoder) *OneDReader {return &OneDReader{rowDecoder}}func (this *OneDReader) Decode(image *gozxing.BinaryBitmap, 略) (*gozxing.Result, error) {...result, e := this.DecodeRow(rowNumber, row, hints)...}Readerインターフェイスの実装埋め込んだRowDecoderのDecodeRowの呼び出しオーバーライドするメソッドを集めたインターフェイス(仮想関数テーブル)仮想関数テーブルの埋め込み初期化関数で実装を要求type code128Reader struct {*OneDReader}func NewCode128Reader() gozxing.Reader {this := &code128Reader{}this.OneDReader = NewOneDReader(this)return this}func (*code128Reader) DecodeRow(略) (*gozxing.Result, error) {...}継承元を埋め込み仮想関数テーブルとして自身を渡す派生クラスのDecodeRow実装派生クラス
メソッドオーバーライドのテクニック● 継承元構造体○ 仮想関数テーブルとなるインターフェイスを持つ■ 派生構造体のメソッドを呼び出せる○ 初期化関数で実装を要求できる● 派生構造体○ 継承元構造体を埋め込む■ 継承元のフィールドやメソッドを利用できる○ 継承元構造体の初期化関数に自身を渡す■ メソッドオーバーライド
例外
JavaからGoへの書き換えtry {alignmentPattern = findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i);break;} catch (NotFoundException e) {// try next round}alignmentPattern, e = this.findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i)if e == nil {break}if _, ok := e.(gozxing.NotFoundException); !ok {return nil, e}
JavaからGoへの書き換えtry {alignmentPattern = findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i);break;} catch (NotFoundException e) {// try next round}alignmentPattern, e = this.findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i)if e == nil {break}if _, ok := e.(gozxing.NotFoundException); !ok {return nil, e}NotFoundExceptionだけ握りつぶす(それ以外は送出)NotFoundException以外を送出例外非発生でbreak (for文の中)nilチェックしてOKならbreak戻り値にerrorを追加
JavaからGoへの書き換えtry {...} catch (FormatException | ChecksumException e) {// 特定の例外種別専用の処理...}...switch e.(type) {case nil: // breakcase gozxing.FormatException, gozxing.ChecksumException:// 特定の例外種別専用の処理default:return nil, e}
JavaからGoへの書き換えtry {...} catch (FormatException | ChecksumException e) {// 特定の例外種別専用の処理...}...switch e.(type) {case nil: // breakcase gozxing.FormatException, gozxing.ChecksumException:// 特定の例外種別専用の処理default:return nil, e}2種類の例外だけ捕捉して特別処理2種類の例外だけ捕捉して特別処理他の例外は明示的に送出例外非発生なら明示的になにもしない
Goに移植するときのポイント● 例外はthrowではなく戻り値として送出する○ error型○ 例外を投げうるメソッドにはすべて戻り値を足す○ 呼び出し側でnilチェック● 型による例外種別判定○ 型アサーション、タイプスイッチ○ 元のJavaの設計そのまま● 例外の階層的なグルーピング○ 継承による型の階層関係
interface継承による例外型の階層関係の模倣● Javaでの継承関係と同じにする○ errorを継承してerrorとしても扱う● 実体はerrorを埋め込んだ構造体type Exception interface {errorexception()}type ReaderException interface {ExceptionreaderException()}type NotFoundException interface {ReaderExceptionnotFoundException()}type notFoundException struct { error }func (_ notFoundException) exception() {}func (_ notFoundException) readerException() {}func (_ notFoundException) notFoundException(){}ExceptionReaderException WriterExceptionNotFoundExceptionFormatException
型による例外種別判定● 型アサーションによる判定● 型スイッチによる判定○ 複数catchがある場合などに便利欠点● ラップされると型が変わる○ xerrorsのErrorfの ": %w"○ 型で判定できなくなるfunc DoSomething() error {return notFoundException{xerrors.New("エラー"),}}...err := DoSomething()if err != nil {if _, ok := err.(ReaderException); ok {// 何か処理}}switch err.(type) {case nil: // breakcase NotFoundException:// 何か処理}
xerrors.As関数による判定も可能● xerrors.As関数の実装○ 次の条件に合うまで Unwrap()■ targetの型に代入可能■ err.As(target) がtrue● 代入可能かを利用した判定○ targetにはinterface型も渡せる○ interface継承の例外型で使える● Asメソッドによる判定○ 今回は省略○ 例外の型ごとに柔軟に設定できるfunc DoSomething() error {return notFoundException{errors.New("エラー"),}}func DoSomething2() error {err := DoSomething()return xerrors.Errorf("エラー2: %w", err)}...err := DoSomething2()if err != nil {var exception ReaderExceptionif xerrors.As(err, &exception) {// 何か処理}}
まとめ
今日紹介したテクニック● クラスと継承○ メソッドオーバーライドの模倣■ 仮想関数テーブルインターフェイス● 例外機構○ 例外型の階層関係の模倣■ インターフェイス継承○ 型による例外種別判定■ 型アサーションやタイプスイッチ■ xerrors.As関数
おしまい質問の他、もっと良い書き方の提案などあればお願いします。