Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 11667] ファイルの概要

このコミットは、Go言語の標準ライブラリであるgo/tokenパッケージから、encoding/gobパッケージへの直接的な依存関係を削除することを目的としています。これにより、go/tokenパッケージのシリアライズ/デシリアライズ機能がより汎用的になり、特定のシリアライズ形式に縛られずに利用できるようになります。具体的には、FileSet構造体の読み書きメソッドが、具体的なio.Readerio.Writerではなく、シリアライズ/デシリアライズのロジックをカプセル化した関数を受け取るように変更されています。

コミット

  • コミットハッシュ: 668418d1227aeb01782ba0ee05ac4ba657c0b5a2
  • 作者: Robert Griesemer gri@golang.org
  • コミット日時: 2012年2月6日 月曜日 17:41:19 -0800
  • コミットメッセージ:
    go/token: remove dependency on encoding/gob
    
    R=dsymonds
    CC=golang-dev
    https://golang.org/cl/5636053
    

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/668418d1227aeb01782ba0ee05ac4ba657c0b5a2

元コミット内容

commit 668418d1227aeb01782ba0ee05ac4ba657c0b5a2
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Feb 6 17:41:19 2012 -0800

    go/token: remove dependency on encoding/gob
    
    R=dsymonds
    CC=golang-dev
    https://golang.org/cl/5636053
---
 src/cmd/godoc/index.go             | 10 ++++++++--
 src/pkg/go/token/serialize.go      | 26 ++++++--------------------\
 src/pkg/go/token/serialize_test.go | 11 +++++++++--
 3 files changed, 23 insertions(+), 24 deletions(-)

diff --git a/src/cmd/godoc/index.go b/src/cmd/godoc/index.go
index daf1bc2cc1..f5b531b054 100644
--- a/src/cmd/godoc/index.go
+++ b/src/cmd/godoc/index.go
@@ -867,7 +867,10 @@ func (x *Index) Write(w io.Writer) error {
 		return err
 	}
 	if fulltext {
-		if err := x.fset.Write(w); err != nil {
+		encode := func(x interface{}) error {
+			return gob.NewEncoder(w).Encode(x)
+		}
+		if err := x.fset.Write(encode); err != nil {
 			return err
 		}
 		if err := x.suffixes.Write(w); err != nil {
@@ -897,7 +900,10 @@ func (x *Index) Read(r io.Reader) error {
 			return err
 		}
 		x.suffixes = new(suffixarray.Index)
-		if err := x.suffixes.Read(r); err != nil {
+		decode := func(x interface{}) error {
+			return gob.NewDecoder(r).Decode(x)
+		}
+		if err := x.suffixes.Read(decode); err != nil {
 			return err
 		}
 	}
diff --git a/src/pkg/go/token/serialize.go b/src/pkg/go/token/serialize.go
index 042d6abdf9..4adc8f9e33 100644
--- a/src/pkg/go/token/serialize.go
+++ b/src/pkg/go/token/serialize.go
@@ -4,11 +4,6 @@
 
 package token
 
-import (
-	"encoding/gob"
-	"io"
-)
-
 type serializedFile struct {
 	// fields correspond 1:1 to fields with same (lower-case) name in File
 	Name  string
@@ -23,19 +18,10 @@ type serializedFileSet struct {
 	Files []serializedFile
 }
 
-func (s *serializedFileSet) Read(r io.Reader) error {
-	return gob.NewDecoder(r).Decode(s)
-}
-
-func (s *serializedFileSet) Write(w io.Writer) error {
-	return gob.NewEncoder(w).Encode(s)
-}
-
-// Read reads the fileset from r into s; s must not be nil.\n// If r does not also implement io.ByteReader, it will be wrapped in a bufio.Reader.
-func (s *FileSet) Read(r io.Reader) error {\n+// Read calls decode to deserialize a file set into s; s must not be nil.\n+func (s *FileSet) Read(decode func(interface{}) error) error {\n 	var ss serializedFileSet
-\tif err := ss.Read(r); err != nil {\n+\tif err := decode(&ss); err != nil {\
 		return err
 	}
 
@@ -53,8 +39,8 @@ func (s *FileSet) Read(r io.Reader) error {\
 	return nil
 }
 
-// Write writes the fileset s to w.\n-func (s *FileSet) Write(w io.Writer) error {\n+// Write calls encode to serialize the file set s.\n+func (s *FileSet) Write(encode func(interface{}) error) error {\
 	var ss serializedFileSet
 
 	s.mutex.Lock()
@@ -66,5 +52,5 @@ func (s *FileSet) Write(w io.Writer) error {\
 	ss.Files = files
 	s.mutex.Unlock()
 
-\treturn ss.Write(w)\n+\treturn encode(ss)\n }\ndiff --git a/src/pkg/go/token/serialize_test.go b/src/pkg/go/token/serialize_test.go
index a8ce30ab2f..4e925adb6f 100644
--- a/src/pkg/go/token/serialize_test.go
+++ b/src/pkg/go/token/serialize_test.go
@@ -6,6 +6,7 @@ package token
 
 import (
 	"bytes"
+	"encoding/gob"
 	"fmt"
 	"testing"
 )
@@ -69,12 +70,18 @@ func equal(p, q *FileSet) error {\
 
 func checkSerialize(t *testing.T, p *FileSet) {
 	var buf bytes.Buffer
-\tif err := p.Write(&buf); err != nil {\n+\tencode := func(x interface{}) error {\n+\t\treturn gob.NewEncoder(&buf).Encode(x)\n+\t}\n+\tif err := p.Write(encode); err != nil {\
 		t.Errorf("writing fileset failed: %s", err)
 		return
 	}
 	q := NewFileSet()
-\tif err := q.Read(&buf); err != nil {\n+\tdecode := func(x interface{}) error {\n+\t\treturn gob.NewDecoder(&buf).Decode(x)\n+\t}\n+\tif err := q.Read(decode); err != nil {\
 		t.Errorf("reading fileset failed: %s", err)
 		return
 	}

変更の背景

この変更の主な背景は、go/tokenパッケージの汎用性と再利用性を高めることにあります。

  1. 依存関係の疎結合化: 以前のgo/tokenパッケージは、FileSet構造体のシリアライズ/デシリアライズにencoding/gobパッケージを直接使用していました。これは、go/tokenパッケージが特定のシリアライズ形式に強く結合していることを意味します。この結合を解消することで、go/tokenパッケージはgob以外のシリアライズ形式(例: JSON, Protocol Buffers, XML, あるいはカスタムバイナリ形式)を使用したいアプリケーションからも利用しやすくなります。
  2. 柔軟性の向上: go/tokenパッケージがシリアライズ/デシリアライズの具体的な実装を内部に持たず、外部からそのロジックを注入できるようにすることで、パッケージの利用者は自身の要件に合わせてシリアライズ方法を選択できるようになります。例えば、パフォーマンス要件が厳しい場合や、異なるシステムとの連携のために特定のデータ形式が必要な場合に、柔軟に対応できます。
  3. パッケージの責務の明確化: go/tokenパッケージの主要な責務は、Goソースコードのトークンと位置情報を管理することです。シリアライズの具体的な方法は、その責務の範囲外と見なすことができます。この変更により、go/tokenパッケージは自身のコアな機能に集中し、シリアライズの詳細は呼び出し元に委ねるという、よりクリーンな設計原則に沿うことになります。
  4. 将来的な拡張性: 特定のシリアライズ形式に依存しないことで、将来的にGo言語や関連ツールが新しいシリアライズ技術を採用した場合でも、go/tokenパッケージのコードを変更することなく対応できるようになります。

前提知識の解説

Go言語

Go(Golang)は、Googleによって開発されたオープンソースのプログラミング言語です。静的型付け、コンパイル型、並行処理のサポート、ガベージコレクションなどの特徴を持ち、シンプルさ、効率性、信頼性を重視して設計されています。システムプログラミング、Webサービス、ネットワークツールなど、幅広い分野で利用されています。

go/tokenパッケージ

go/tokenは、Go言語の標準ライブラリの一部であり、Goソースコードの字句解析(トークン化)や構文解析において、ソースコード上の位置情報(ファイル名、行番号、列番号、オフセット)を管理するためのパッケージです。

  • File: 個々のソースファイルに関する位置情報(ファイル名、サイズ、行オフセットなど)を保持します。
  • FileSet: 複数のFileオブジェクトをまとめて管理するコンテナです。これにより、複数のファイルにまたがるソースコード全体での位置情報を一貫して扱うことができます。コンパイラやリンター、コードフォーマッターなど、Goのコードを解析・操作するツールで広く利用されます。

encoding/gobパッケージ

encoding/gobは、Go言語の標準ライブラリの一つで、Goのデータ構造をバイナリ形式でシリアライズ(符号化)およびデシリアライズ(復号化)するためのパッケージです。

  • シリアライズ: プログラム内のオブジェクト(データ構造)を、ファイルに保存したり、ネットワーク経由で送信したりするために、バイト列の形式に変換するプロセスです。
  • デシリアライズ: シリアライズされたバイト列を、元のオブジェクトの形式に復元するプロセスです。

gobは、Goプログラム間でGoの値を効率的に転送するのに特に適しています。データ型情報も一緒にエンコードされるため、デシリアライズ時に型情報が自動的に利用されます。

依存性の注入 (Dependency Injection)

依存性の注入(DI)は、ソフトウェア設計パターンの一つで、オブジェクトが依存する他のオブジェクト(依存性)を、オブジェクト自身が生成するのではなく、外部から提供(注入)されるようにする手法です。これにより、コンポーネント間の結合度を低減し、コードの再利用性、テスト容易性、保守性を向上させることができます。

今回のコミットでは、go/tokenパッケージがencoding/gobに直接依存する代わりに、シリアライズ/デシリアライズの具体的なロジックを外部から関数として受け取るように変更されています。これは、シリアライズの「依存性」をgo/tokenパッケージの外部から「注入」する形にすることで、パッケージの内部実装から特定のシリアライズ形式への結合を排除する、一種の依存性注入と見なすことができます。

技術的詳細

このコミットの技術的な核心は、go/tokenパッケージ内のFileSet構造体のシリアライズ/デシリアライズメカニズムを、特定のencoding/gob実装から抽象化し、汎用的な関数インターフェースに置き換えた点にあります。

変更前、FileSetは内部でencoding/gobを直接利用し、io.Readerio.Writerを介して自身の状態を読み書きしていました。これは、FileSetgob形式でのみシリアライズ/デシリアライズ可能であることを意味し、他の形式での利用を困難にしていました。

変更後、FileSet.ReadおよびFileSet.Writeメソッドのシグネチャが以下のように変更されました。

  • 変更前:
    func (s *FileSet) Read(r io.Reader) error
    func (s *FileSet) Write(w io.Writer) error
    
  • 変更後:
    func (s *FileSet) Read(decode func(interface{}) error) error
    func (s *FileSet) Write(encode func(interface{}) error) error
    

新しいシグネチャでは、io.Readerio.Writerの代わりに、func(interface{}) error型の関数decodeencodeを引数として受け取ります。

  • encode func(interface{}) error: この関数は、任意のGoの値を引数として受け取り、それをバイトストリームにエンコードして書き込む責務を持ちます。
  • decode func(interface{}) error: この関数は、バイトストリームからデータを読み込み、それを任意のGoの値にデコードして引数に渡されたインターフェースに格納する責務を持ちます。

これにより、go/tokenパッケージ自体は、データのエンコード/デコードの具体的な方法を知る必要がなくなりました。その代わりに、呼び出し元が、gobjsonxml、あるいはカスタムのシリアライザなど、任意のシリアライズメカニズムに対応するencodeおよびdecode関数を提供できるようになります。

この変更に伴い、src/cmd/godoc/index.goでは、FileSetのシリアライズ/デシリアライズに引き続きgobを使用するために、以下のようなラッパー関数(クロージャ)が導入されました。

// godoc/index.go の変更箇所
// Writeメソッド内
encode := func(x interface{}) error {
    return gob.NewEncoder(w).Encode(x)
}
if err := x.fset.Write(encode); err != nil { ... }

// Readメソッド内
decode := func(x interface{}) error {
    return gob.NewDecoder(r).Decode(x)
}
if err := x.suffixes.Read(decode); err != nil { ... }

これらのクロージャは、io.Writer (w) や io.Reader (r) を使用してgob.NewEncodergob.NewDecoderを初期化し、そのEncodeDecodeメソッドをFileSet.WriteFileSet.Readが期待するfunc(interface{}) errorシグネチャに適合させています。これにより、godocコマンドは、go/tokenパッケージの変更後も、既存のgob形式のインデックスファイルを問題なく読み書きできるようになっています。

また、src/pkg/go/token/serialize.goからは、encoding/gobioのインポートが削除され、serializedFileSet構造体に対するReadWriteメソッドも削除されました。これは、FileSetのシリアライズロジックがserializedFileSetの内部メソッドから、FileSet自身のメソッドに直接移動し、かつその実装が外部から注入される関数に委ねられるようになったためです。

テストコード(src/pkg/go/token/serialize_test.go)も、この新しいインターフェースに合わせて修正され、テスト内で明示的にgob.NewEncodergob.NewDecoderを作成し、そのEncode/DecodeメソッドをFileSet.Write/FileSet.Readに渡す形になっています。これにより、go/tokenパッケージのシリアライズ機能が正しく動作することを確認しています。

コアとなるコードの変更箇所

src/pkg/go/token/serialize.go

このファイルは、go/tokenパッケージにおけるFileSetのシリアライズ/デシリアライズロジックを定義しています。

  • インポートの削除:

    --- a/src/pkg/go/token/serialize.go
    +++ b/src/pkg/go/token/serialize.go
    @@ -4,11 +4,6 @@
     
     package token
     
    -import (
    -	"encoding/gob"
    -	"io"
    -)
    

    encoding/gobioパッケージへの直接的な依存が削除されました。

  • serializedFileSetRead/Writeメソッドの削除: serializedFileSetFileSetの内部表現をシリアライズ可能にした構造体ですが、これに対するgobベースのRead/Writeメソッドが削除されました。

    --- a/src/pkg/go/token/serialize.go
    +++ b/src/pkg/go/token/serialize.go
    @@ -23,19 +18,10 @@ type serializedFileSet struct {
     	Files []serializedFile
     }
     
    -func (s *serializedFileSet) Read(r io.Reader) error {
    -	return gob.NewDecoder(r).Decode(s)
    -}
    -
    -func (s *serializedFileSet) Write(w io.Writer) error {
    -	return gob.NewEncoder(w).Encode(s)
    -}
    
  • FileSet.Readメソッドのシグネチャ変更と実装修正: io.Readerを受け取る代わりに、decode func(interface{}) error関数を受け取るようになりました。

    --- a/src/pkg/go/token/serialize.go
    +++ b/src/pkg/go/token/serialize.go
    @@ -23,19 +18,10 @@ type serializedFileSet struct {
     	Files []serializedFile
     }
     
    -func (s *serializedFileSet) Read(r io.Reader) error {
    -	return gob.NewDecoder(r).Decode(s)
    -}
    -
    -func (s *serializedFileSet) Write(w io.Writer) error {
    -	return gob.NewEncoder(w).Encode(s)
    -}
    -
    -// Read reads the fileset from r into s; s must not be nil.\n// If r does not also implement io.ByteReader, it will be wrapped in a bufio.Reader.
    -func (s *FileSet) Read(r io.Reader) error {\n+// Read calls decode to deserialize a file set into s; s must not be nil.
    +func (s *FileSet) Read(decode func(interface{}) error) error {
     	var ss serializedFileSet
    -\tif err := ss.Read(r); err != nil {\n+\tif err := decode(&ss); err != nil {
     		return err
     	}
    
  • FileSet.Writeメソッドのシグネチャ変更と実装修正: io.Writerを受け取る代わりに、encode func(interface{}) error関数を受け取るようになりました。

    --- a/src/pkg/go/token/serialize.go
    +++ b/src/pkg/go/token/serialize.go
    @@ -53,8 +39,8 @@ func (s *FileSet) Read(r io.Reader) error {\
     	return nil
     }
     
    -// Write writes the fileset s to w.\n-func (s *FileSet) Write(w io.Writer) error {\n+// Write calls encode to serialize the file set s.
    +func (s *FileSet) Write(encode func(interface{}) error) error {
     	var ss serializedFileSet
     
     	s.mutex.Lock()
    @@ -66,5 +52,5 @@ func (s *FileSet) Write(w io.Writer) error {\
     	ss.Files = files
     	s.mutex.Unlock()
     
    -\treturn ss.Write(w)\n+\treturn encode(ss)
     }
    

src/cmd/godoc/index.go

godocコマンドはgo/tokenパッケージを利用しているため、FileSetのシリアライズ/デシリアライズ方法の変更に合わせて、このファイルも修正されました。

  • Index.Writeメソッド内の変更: x.fset.Writeを呼び出す際に、gob.NewEncoderを使用してencodeクロージャを作成し、それを渡すように変更されました。

    --- a/src/cmd/godoc/index.go
    +++ b/src/cmd/godoc/index.go
    @@ -867,7 +867,10 @@ func (x *Index) Write(w io.Writer) error {
     		return err
     	}
     	if fulltext {
    -		if err := x.fset.Write(w); err != nil {
    +		encode := func(x interface{}) error {
    +			return gob.NewEncoder(w).Encode(x)
    +		}
    +		if err := x.fset.Write(encode); err != nil {
     			return err
     		}
     		if err := x.suffixes.Write(w); err != nil {
    
  • Index.Readメソッド内の変更: x.suffixes.Readを呼び出す際に、gob.NewDecoderを使用してdecodeクロージャを作成し、それを渡すように変更されました。

    --- a/src/cmd/godoc/index.go
    +++ b/src/cmd/godoc/index.go
    @@ -897,7 +900,10 @@ func (x *Index) Read(r io.Reader) error {
     			return err
     		}
     		x.suffixes = new(suffixarray.Index)
    -		if err := x.suffixes.Read(r); err != nil {
    +		decode := func(x interface{}) error {
    +			return gob.NewDecoder(r).Decode(x)
    +		}
    +		if err := x.suffixes.Read(decode); err != nil {
     			return err
     		}
     	}
    

src/pkg/go/token/serialize_test.go

テストコードも、新しいFileSet.WriteおよびFileSet.Readのシグネチャに合わせて更新されました。

  • checkSerialize関数内の変更: テスト内でgob.NewEncodergob.NewDecoderを明示的に作成し、そのEncode/DecodeメソッドをFileSet.Write/FileSet.Readに渡すように修正されました。
    --- a/src/pkg/go/token/serialize_test.go
    +++ b/src/pkg/go/token/serialize_test.go
    @@ -6,6 +6,7 @@ package token
     
     import (
     	"bytes"
    +	"encoding/gob"
     	"fmt"
     	"testing"
     )
    @@ -69,12 +70,18 @@ func equal(p, q *FileSet) error {\
     
     func checkSerialize(t *testing.T, p *FileSet) {
     	var buf bytes.Buffer
    -\tif err := p.Write(&buf); err != nil {\n+\tencode := func(x interface{}) error {
    +\t\treturn gob.NewEncoder(&buf).Encode(x)
    +\t}
    +\tif err := p.Write(encode); err != nil {
     		t.Errorf("writing fileset failed: %s", err)
     		return
     	}
     	q := NewFileSet()
    -\tif err := q.Read(&buf); err != nil {\n+\tdecode := func(x interface{}) error {
    +\t\treturn gob.NewDecoder(&buf).Decode(x)
    +\t}
    +\tif err := q.Read(decode); err != nil {
     		t.Errorf("reading fileset failed: %s", err)
     		return
     	}
    

コアとなるコードの解説

このコミットのコアとなる変更は、go/tokenパッケージが特定のシリアライズ形式(この場合はencoding/gob)に直接依存するのをやめ、シリアライズ/デシリアライズのロジックを外部から注入可能な関数として受け入れるようにした点です。

具体的には、FileSet構造体のReadおよびWriteメソッドが、io.Readerio.Writerといったストリームインターフェースを直接扱うのではなく、func(interface{}) errorという関数シグネチャを持つdecodeおよびencode関数を引数として受け取るように変更されました。

  • 抽象化の実現: これにより、go/tokenパッケージは、データの具体的なバイト列への変換方法や、バイト列からの復元方法について関知する必要がなくなりました。パッケージの内部ロジックは、単にFileSetの内部表現(serializedFileSet)を、提供されたencode関数を使ってエンコードし、提供されたdecode関数を使ってデコードするだけになります。
  • 柔軟なシリアライズ形式の選択: この設計変更により、go/tokenパッケージの利用者は、自身のアプリケーションの要件に応じて、gobjsonxmlprotobufなど、任意のシリアライズ形式を選択し、それに対応するencode/decode関数をFileSet.Write/FileSet.Readに渡すことができるようになります。例えば、godocコマンドは引き続きgobを使用していますが、他のツールがFileSetを異なる形式で保存したい場合でも、go/tokenパッケージのコードを変更することなく対応可能です。
  • テスト容易性の向上: シリアライズロジックが外部から注入可能になったことで、テスト時にも特定のシリアライズ形式に依存せず、モックやスタブのencode/decode関数を渡して、FileSetのシリアライズ/デシリアライズ動作をより容易にテストできるようになります。
  • 責務の分離: go/tokenパッケージは、Goソースコードのトークンと位置情報を管理するという本来の責務に集中し、データの永続化や転送といったシリアライズの責務は、呼び出し元に委ねられる形になりました。これは、ソフトウェア設計における「単一責任の原則」に合致し、コードベースの健全性を高めます。

この変更は、Go言語の標準ライブラリが、特定の技術や実装に強く依存するのではなく、より汎用的で柔軟なインターフェースを提供する方向性を示していると言えます。

関連リンク

参考にした情報源リンク