デイブ・チェイニーによる記事の翻訳-ピーター・バーゴンの以前の投稿「The Modern Theory of Modern Go」への回答-ゴーが一般にグローバルな範囲で変数なしで見ているかのように、思考実験を試みた。 一部の段落では言語を破ることができますが、投稿は非常に興味深いものです。
パッケージのグローバルスコープ内の変数を削除した場合にGoがどのようになるかを考えて、実験してみましょう。 結果はどうなりますか?この実験からGoプログラムの設計について何を学ぶことができますか?
var
削除についてのみ説明します。他の5つのトップレベルの定義は、実際にはコンパイル段階で定数なので、実験では許可されたままです。 そしてもちろん、関数やブロックで変数を定義し続けることができます。
パッケージのグローバル変数が悪いのはなぜですか?
しかし、まず、パッケージのグローバル変数がなぜ悪いのかという質問に答えましょう。 組み込みの並行性を備えた言語のグローバルな可視状態の明らかな問題は別として、パッケージ内のグローバル変数は、本質的に、弱くあまり接続されていないものの間で状態を暗黙的に変更するために使用されるシングルトーンであり、強力な依存関係を作成し、コードのテストを困難にします。
Peter Burgonが最近書いたように :
tl;博士の魔法は悪いです。 グローバル状態は魔法です→パッケージ内のグローバル変数は不良です。 init()関数は必要ありません。
実際にグローバル変数を取り除く
このアイデアをテストするために、Goで最も人気のあるコードベースである標準ライブラリを詳細に調べ、パッケージでグローバル変数を使用する方法を確認し、実験の効果を評価しようとしました。
間違い
パブリックパッケージでのグローバルio.EOF
の最も一般的な使用法の1つはエラーですio.EOF
、 sql.ErrNoRows
、 crypto/x509.ErrUnsupportedAlgorithm
など。 これらの変数がないと、エラーを事前定義された値と比較できません。 しかし、それらを何かに置き換えることはできますか?
エラーを分析するときは、タイプではなく動作を調べるようにしてくださいと以前に書きました。 これが不可能な場合、エラーの定数の定義により、エラーの修正の可能性が排除され、セマンティクスが保持されます。
エラーの残りの変数はプライベートであり、単にエラーメッセージにシンボル名を付けます。 これらの変数はエクスポートできないため、パッケージ外部からの比較には使用できません。 それらが発生する場所ではなく、パッケージの最上位で定義すると、エラーにコンテキストを追加できなくなります。 代わりに、 pkg / errorsのようなものを使用して、発生時のスタックトレースをエラーに保存することをお勧めします。
登録
登録パターンは、標準ライブラリのいくつかのパッケージ( net/http
、 database/sql
、 flag
でlog
れ、 log
でも少し使用されlog
。 通常、マップタイプまたは構造のグローバル変数で構成されます。これは、いくつかのパブリック関数(クラシックシングルトン)によって変更されます。
外部から初期化する必要のあるこのようなダミー変数を作成できないため、 image
、 database/sql
、およびcrypto
パッケージは、デコーダー、データベースドライバー、および暗号化スキームを登録できません。 しかし、これはPeterが彼の記事で説明しているのとまったく同じ魔法です。パッケージをインポートして、別のパッケージのグローバル状態を暗黙的に変更し、外部からは不吉に見えます。
登録は、ビジネスロジックの繰り返しも促進します。 たとえば、 net/http/pprof
それ自体を登録し、副作用としてnet/http.DefaultServeMux
使用しますが、これは完全に安全ではありません-別のコードでは、pprofが提供する情報を使用せずにデフォルトのマルチプレクサを使用できなくなります-別のマルチプレクサに登録するのはそれほど簡単ではありません。
パケットにグローバル変数がない場合、 net/http/pprof
などのパケットは、特定のhttp.ServeMux
URLパスを登録し、別のパケットのグローバル状態の暗黙的な変更に依存しない関数を提供できます 。
登録パターンを使用する可能性をなくすことは、同じパッケージの複数のコピーの問題を解決するのに役立つ可能性があり、インポートされると、すべてが最初に一緒に登録しようとします。
インターフェース満足度チェック
インターフェイスが型に属するかどうかをチェックするためのこのようなイディオムがあります:
var _ SomeInterface = new(SomeType)
標準ライブラリで少なくとも19回検出されます。 私の意見では、そのようなチェックは実際にはテストです。 パッケージをビルドするときに後で削除するために、これらをコンパイルしないでください。 対応する_test.go
ファイルに_test.go
する必要があります。 しかし、パッケージ内のグローバル変数を禁止する場合、これはテストにも適用されるので、このチェックをどのように維持できますか?
1つの解決策は、この変数定義をグローバルスコープから関数のスコープに移すことです。SomeTypeが突然SomeInterface
を満たすのをやめた場合、コンパイルは停止しSomeInterface
func TestSomeTypeImplementsSomeInterface(t *testing.T) { // won't compile if SomeType does not implement SomeInterface var _ SomeInterface = new(SomeType) }
しかし、これは実際には単なるテストであるため、このイディオムを通常のテストの形式に書き換えることができます。
func TestSomeTypeImplementsSomeInterface(t *testing.T) { var i interface{} = new(SomeType) if _, ok := i.(SomeInterface); !ok { t.Fatalf("expected %t to implement SomeInterface", i) } }
Go仕様では、空の識別子(_)への割り当ては割り当て記号の右側の式を完全に評価することを意味するため、ここではグローバルスコープ内の疑わしい初期化がいくつか隠されている可能性があります。
しかし、それほど単純ではありません
前のセクションでは、すべてがスムーズに進み、グローバル変数を削除する実験は成功したようですが、標準ライブラリにはすべてがそれほど単純ではない場所がいくつかあります。
リアルシングルトン
全体としてシングルトンパターンは、特に登録など、必要のない場所で使用されることが多いと考えていますが、各プログラムには常に実際のシングルトンが存在します。 これの良い例は、 os.Stdout
と会社です。
package os var ( Stdin = NewFile(uintptr(syscall.Stdin), "/dev/stdin") Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout") Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr") )
この定義にはいくつかの問題があります。 まず、 Stdin
、 Stdout
およびStderr
は、 io.Reader
またはio.Writer
インターフェースではなく、 *os.File
型の変数です。 これにより、それらを代替品と置き換えるのは非常に問題が多いです。 しかし、それらを置き換えるというまさにアイデアさえ、まさに私たちの実験が取り除こうとしている魔法です。
一定のエラーを伴う前の例が示したように、標準のIO記述子用にシングルトンエンティティを残すことができるため、 log
やfmt
などのパッケージはそれらを直接使用できますが、可変グローバル変数として宣言できません。 このようなもの:
package main import ( "fmt" "syscall" ) type readfd int func (r readfd) Read(buf []byte) (int, error) { return syscall.Read(int(r), buf) } type writefd int func (w writefd) Write(buf []byte) (int, error) { return syscall.Write(int(w), buf) } const ( Stdin = readfd(0) Stdout = writefd(1) Stderr = writefd(2) ) func main() { fmt.Fprintf(Stdout, "Hello world") }
キャッシュ
パッケージでエクスポートされていないグローバル変数を使用する2番目に最も一般的な方法は、キャッシュです。 それらは、マップタイプ(上記の登録パターンを参照)またはsync.Pool
のオブジェクトで構成される実際のキャッシュと、コンパイルコストを改善する準sync.Pool
変数(翻訳者のメモ-「sht?」)の2つのタイプです。
例としては、 crypto/ecsda
がありcrypto/ecsda
。このcrypto/ecsda
には、入力として渡されるすべてのバッファーをRead()メソッドがリセットするタイプzr
があります。 パッケージには、io.Readerなどの他の構造に組み込まれ、宣言されるたびにヒープにエスケープされる可能性があるため、zr型の単一の変数が含まれています。
package ecdsa type zr struct { io.Reader } // Read replaces the contents of dst with zeros. func (z *zr) Read(dst []byte) (n int, err error) { for i := range dst { dst[i] = 0 } return len(dst), nil } var zeroReader = &zr{}
しかし、同時に、 zr
型には組み込みのio.Reader
が含まれていませんzr
io.Reader
実装しているため、未使用のzr.Reader
フィールドを削除して、 zr
を空の構造にすることができます。 私のテストでは、この変更された型は、パフォーマンスを失うことなく明示的に初期化できます。\
csprng := cipher.StreamReader{ R: zr{}, S: cipher.NewCTR(block, []byte(aesIV)), }
インライン化とエスケープの分析は、標準ライブラリが作成されてから非常に顕著に改善されているため、おそらく、いくつかのキャッシュソリューションを確認するのが理にかなっています。
テーブル
そして、パッケージ内でプライベートグローバル変数を最後に最も頻繁に使用するのはテーブルです-たとえば、 unicode
、 crypto/*
およびmath
パッケージ。 これらのテーブルは通常、定数データを整数配列としてエンコードしますが、まれに単純な構造またはマップ型のオブジェクトとしてエンコードします。
グローバル変数を定数で置き換えるには、言語の変更が必要になります 。これは、ここで説明したものと似ています 。 したがって、プログラムの実行中にこれらのテーブルを変更する方法がないと考える場合、これらはこの提案の例外になる可能性があります。
行き過ぎ
この投稿は単なる思考実験であるという事実にもかかわらず、パッケージ内のすべてのグローバル変数の禁止が言語で現実的であるにはあまりにも厳しい基準であることはすでに明らかです。 禁止の問題を回避することは、パフォーマンスの観点から非常に非現実的である可能性があり、「ヒットミー」ポスターを背中に掛けて、Goの嫌いな人全員に楽しんでもらうようなものです。
しかし、同時に、極端に進んで言語仕様を変更することなく、この思考実験から引き出すことができるいくつかの非常に具体的なヒントがあるように思えます。
- まず、パブリック
var
定義の使用は避けるのが最善です。 これは物議を醸すトピックではなく、Goに固有のものではありません。 シングルトンパターンを使用しないことをお勧めします。名前を知っている人がいつでも変更できる泥だらけのパブリック変数は、自動的に停止信号になります。 - 第二に、パブリック変数がどこかで定義されている場合、その型に非常に注意し、できるだけ単純にするようにする必要があります。 理論上、型がインスタンスごとに使用されるようなものであってはなりません(翻訳者の注意-正しく解釈する方法がわかりません)が、パッケージのグローバルスコープの変数に割り当てます。
グローバル変数のプライベート定義はより具体的ですが、いくつかのパターンを抽出できます。
- 私が「レジストリ」と呼ぶパブリックセッター(Set()関数)を持つプライベート変数は、基本的にパブリックセッターと同じ効果があります。 グローバルスコープに依存関係を登録する代わりに、作成時にコンストラクター関数、リテラル、構成構造、またはオプション関数を使用して渡す必要があります 。
- タイプ[]バイトの変数の形式のキャッシュは、多くの場合、パフォーマンスを損なうことなく定数として定義できます。 コンパイラは、関数の範囲を超えない
string([]byte)
ように、呼び出しを最適化することを忘れないでください。 -
unicode
パッケージなどのテーブルを含むプライベート変数は、Goに定数配列型がないために避けられない結果です。 それらがプライベートであり、それらを変更する方法を提供しない限り、実際にこの議論の枠組みで定数と見なすことができます。
プログラムの実行中に値を変更する可能性のあるグローバル変数をパッケージに追加する前に、要約し、よく考えてください。 これは、魔法のグローバルステートが追加されたことを示している可能性があります。