Mercari Advent Calendar 2017 の5日目はソウゾウ エキスパートチームの@tenntennがお送りします。
本日は12月5日、つまりは12月Go日ということでGoの話題について書きたいと思います。
先日、golang.tokyo#10にて”メルカリ カウルのマスタデータの更新“というタイトルで発表を行いました。
その中でgolang.org/x/text/transformパッケージを用いたバイト列の変換について紹介しました。
しかし、golang.tokyo#10では時間の関係上、詳しい説明を省いてしまったため、ここではtransform.Transformer
インタフェースの実装方法について解説を行います。
transformパッケージ
golang.org/x/text/transform
パッケージ(以降transform
パッケージと表記します)は、Goの準標準パッケージであるgolang.org/x
以下で管理されているパッケージです。
transform
パッケージは主にバイト列の変換を行う機能を提供します。
transform
パッケージでは、バイト列の変換処理を一般的に扱うために、Transform
インタフェースを提供しています。
そして、Transform
インタフェースを基にして、*transform.Reader
型や*transform.Writer
型の値を作成することにより、変換処理機能を持ったio.Reader
インタフェースやio.Writer
インタフェースを実装した値を生み出すことができます。
*transform.Reader
型や*transform.Writer
型の値は、次のように内部にtransform.Transformer
インタフェースを実装する値を持つことで、一度にすべてをメモリ上に載せることなく、ストリームのまま変換処理を行うことができます。
encoding
パッケージ
transform.Transformer
インタフェースを実装したものとして、golang.org/x/text/encoding/japaneseパッケージ以下のjapanese.ShiftJIS
やjapanese.EUCJP
などがあります。
これらの変数はencoding.Encoding
型で次のように定義されています。
type Encoding interface { // NewDecoder returns a Decoder. NewDecoder() *Decoder // NewEncoder returns an Encoder. NewEncoder() *Encoder }
encoding.Encoding
インタフェースが持つ2つのメソッドが返すそれぞれの値、*encoding.Decoder
型と*encoding.Encoder
型にはtransform.Transformer
インタフェースが埋め込んであります。
type Decoder struct { transform.Transformer // contains filtered or unexported fields } type Encoder struct { transform.Transformer // contains filtered or unexported fields }
そのため、encoding/japanese
パッケージなどで定義されているEncoding
はtransform.Transformer
インタフェースを実装しているということになります。
そこで、第1引数にio.Reader
インタフェース、第2引数にtransform.Transformer
インタフェースをとるtransform.NewReader
関数に次のように、*encoding.Decoder
型の値を渡すと、読み込んだバイト列をShiftJISとして解釈して読み込むReader
を作ることができます。
func main() {
dec := japanese.ShiftJIS.NewDecoder()
r := transform.NewReader(os.Stdin, dec)
io.Copy(os.Stdout, r)
}
transform.Transformerインタフェースの実装
さて、transform.Transformer
インタフェースを自前で実装するためにはどうすればよいでしょう。
まずはtransform.Transformer
インタフェースの定義についてみてみます。
ドキュメントを見ると次のように定義されていることが分かります。
type Transformer interface { // Transform writes to dst the transformed bytes read from src, and // returns the number of dst bytes written and src bytes read. The // atEOF argument tells whether src represents the last bytes of the // input. // // Callers should always process the nDst bytes produced and account // for the nSrc bytes consumed before considering the error err. // // A nil error means that all of the transformed bytes (whether freshly // transformed from src or left over from previous Transform calls) // were written to dst. A nil error can be returned regardless of // whether atEOF is true. If err is nil then nSrc must equal len(src); // the converse is not necessarily true. // // ErrShortDst means that dst was too short to receive all of the // transformed bytes. ErrShortSrc means that src had insufficient data // to complete the transformation. If both conditions apply, then // either error may be returned. Other than the error conditions listed // here, implementations are free to report other errors that arise. Transform(dst, src []byte, atEOF bool) (nDst, nSrc int, err error) // Reset resets the state and allows a Transformer to be reused. Reset() }
このように、transform.Transformer
インタフェースには、2つのメソッドがあります。
Transform
メソッドはその名の通り、バイト列の変換を行うメソッドです。
一方、Reset
メソッドはこのTransformer
が再利用できるようにリセットするための機能を提供します。
ここではTransform
メソッドについて詳しくみていきましょう。
引数は、変換後のバイト列をいれるdst
と変換元のバイト列が入ったsrc
、EOF
がきたかどうかを表すatEOF
です。
戻り値として変換を行ったバイト列の結果が何バイトになったかを表すnDst
と変換元のバイト列を何バイト変換したかを表すnSrc
、そしてエラーを返します。
引数や戻り値だけを見るとtransform.Transformer
インタフェースの実装は一見簡単そうですが、境界値やイレギュラーな挙動を実装するとなると途端にややこしくなります。
前述の通り、transform.Transformer
インタフェースはストリームのまま、*transform.Reader
型や*transform.Writer
型に利用されます。
そのため、一度にバイト列をメモリ上に乗せて処理することができず、小さく分割して処理する必要があります。
小さく分割するということは、分割をまたいで変換処理を行う必要があるということで、その場合非常に処理がややこしくなります。
たとえば、"Merry Xmas"
という文字列の"Merry"
を"Mercari"
に置換するという処理を考えてみます。
引数src
が3バイトで分割されているとした場合、src
は"Mer"
となります。
これでは残りの部分がどうなっているのか分からないため、"Merry"
にマッチするどうかを判断することができません。
そこでtransform.Transformer
インタフェースでは、戻り値にtransform.ErrShortSrc
という特殊なエラーを返すことで、呼び出し元にsrc
の分割が小さすぎるということを知らせることができます。
一方、変換後の値がdst
に収まらないサイズだった場合、transform.ErrShortDst
というエラーを返すことで、それを知らせます。
このように、バイト列を分割して処理する際には、境界値を気にしながら処理を書く必要があるため非常にややこしくなります。
そのため、テーブル駆動テストでテストケースを網羅的に列挙しながら実装をすすめないと、イレギュラーなバグに悩まされることになるでしょう。
実際の実装されたものに興味のある方は、筆者の開発したgithub.com/tenntenn/text/transformパッケージが参考になります。
このパッケージは任意のバイト列を別のバイト列に変換するためのReplacer
という型を提供しています。
テストもついてますので、どのようなテストケースを準備すればよいのか、transform.Transformer
インタフェースの実装の助けになるでしょう。
まとめ
本記事ではGoでバイト列を変換するために用いるtransform.Transformer
インタフェースについて紹介しました。
実装するのは若干ややこしいですが、任意のバイト列の変換処理をかけたい場合に便利なインタフェースであるため利用できる範囲は大きいでしょう。ぜひ読者のみなさんも手元で実装してみてください。
このようにメルカリ/ソウゾウでは、Goを書く機会がどんどん増えてきております。
それに伴い、slackのGoチャンネルやソウゾウの社内勉強会であるGo Fridayの議論が活発になってきています。
そして、最近発足したGoの可読性をチェックするGo Readability TeamによるコードレビューなどGoエンジニアにとっては良い環境が整ってきています。
そんな環境で私たちと一緒にGoを書いてくれるエンジニアを募集しています!