[インデックス 15659] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージ内のdial.go
ファイルを変更しています。dial.go
は、ネットワーク接続を確立するためのDial
およびDialOpt
関数、およびそれらの動作をカスタマイズするためのDialOption
インターフェースとその実装を定義しています。
コミット
commit 8d51c330122a88a7f6644580c6907ca634357f16
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Sat Mar 9 18:14:00 2013 -0800
net: evaluate the timeout dial opt's deadline at dial time
Previously it was evaluated once, so re-using the timeout option
repeatedly would always generate the same deadline.
Also switch to doing just one pass over the options, making
the private interface actually useful.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/7608045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8d51c330122a88a7f6644580c6907ca634357f16
元コミット内容
net: evaluate the timeout dial opt's deadline at dial time
以前は、タイムアウトオプションが一度評価されると、そのオプションを繰り返し再利用しても常に同じデッドラインが生成されていました。
また、オプションに対する処理を一度のパスで行うように変更し、プライベートインターフェースを実際に有用なものにしました。
変更の背景
このコミットの主な背景は、Go言語のnet
パッケージにおけるDial
操作のタイムアウト処理の不正確さ、特にTimeout
オプションの再利用に関する問題の修正です。
-
タイムアウトデッドラインの固定化: 以前の実装では、
Timeout
オプションがtime.Now().Add(d)
のように、オプションが作成された時点の現在時刻に基づいてデッドラインを計算していました。このため、一度作成されたTimeout
オプションを複数のDial
呼び出しで再利用すると、常に最初のDial
呼び出し時に計算されたデッドラインが適用されてしまい、後続の呼び出しでは期待通りの相対的なタイムアウトではなく、過去の固定されたデッドラインが使われるという問題がありました。これは、特に接続が遅延した場合に、ユーザーが意図したよりも早くタイムアウトが発生したり、逆にタイムアウトしなかったりする原因となっていました。 -
オプション処理の効率化とインターフェースの改善: 以前の
DialOpt
関数は、netFromOptions
、deadlineFromOptions
、localAddrFromOptions
といった複数のヘルパー関数を呼び出し、それぞれがDialOption
のスライスを個別に走査していました。これは非効率であり、またDialOption
インターフェースが持つべき「オプションを設定する」という本来の役割を十分に果たしていませんでした。コミットメッセージにある「making the private interface actually useful」という記述は、DialOption
が単にマーカーインターフェースとして機能するだけでなく、実際にオプションの値を設定するロジックを持つように変更されたことを示唆しています。
これらの問題を解決するため、タイムアウトのデッドラインをDial
呼び出し時に動的に評価するように変更し、オプションの処理を単一のパスで行うようにリファクタリングされました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とネットワークプログラミングの基礎知識が必要です。
- Go言語の
net
パッケージ: Go言語の標準ライブラリで、TCP/IP、UDP、UnixドメインソケットなどのネットワークI/O機能を提供します。Dial
関数は、指定されたネットワークアドレスへの接続を確立するために使用されます。 Dial
関数とDialOpt
関数:net.Dial(network, address string)
: 指定されたネットワークとアドレスに接続します。net.DialOpt(address string, opts ...DialOption)
:Dial
の拡張版で、DialOption
インターフェースを実装するオプションを複数指定して接続動作をカスタマイズできます。
DialOption
インターフェース:DialOpt
関数に渡されるオプションの型を定義するインターフェースです。このコミット以前は、単にdialOption()
というメソッドを持つマーカーインターフェースでした。このコミットでsetDialOpt(*dialOpts)
メソッドを持つように変更され、オプションの値を設定する具体的なロジックを持つようになりました。time.Time
とtime.Duration
:time.Time
: 特定の時点を表すGoの型です。ネットワーク操作におけるデッドライン(期限)を設定する際に使用されます。time.Duration
: 時間の長さを表すGoの型です。タイムアウト期間を指定する際に使用されます。
- ネットワークタイムアウトとデッドライン:
- タイムアウト (Timeout): ある操作が完了するまでの最大許容時間です。この時間を超えると操作は中断され、エラーが返されます。
time.Duration
で指定されます。 - デッドライン (Deadline): ある操作が完了しなければならない絶対的な時刻です。
time.Time
で指定されます。タイムアウトは相対的な期間であるのに対し、デッドラインは絶対的な時点です。
- タイムアウト (Timeout): ある操作が完了するまでの最大許容時間です。この時間を超えると操作は中断され、エラーが返されます。
- 構造体とメソッド: Go言語におけるデータ構造(構造体)と、それに紐付けられた関数(メソッド)の概念です。このコミットでは、
dialOpts
という新しい構造体が導入され、各DialOption
の実装がこの構造体のフィールドを設定するメソッドを持つように変更されています。 - インターフェースとポリモーフィズム:
DialOption
インターフェースは、異なる種類のオプション(ネットワークタイプ、デッドライン、ローカルアドレスなど)を統一的に扱うためのGoの機能です。各オプションはDialOption
インターフェースを実装することで、DialOpt
関数に渡され、それぞれのsetDialOpt
メソッドが呼び出されることで、dialOpts
構造体に適切な値が設定されます。
技術的詳細
このコミットは、net
パッケージのDial
オプション処理を根本的に改善しています。
変更前の問題点:
Timeout
オプションのデッドライン固定化:Timeout(d time.Duration)
関数は、time.Now().Add(d)
を使用してデッドラインを計算し、dialDeadline
型として返していました。このdialDeadline
はtime.Time
をラップしたものであり、一度作成されるとデッドラインの値は固定されていました。そのため、同じTimeout
オプションインスタンスを複数回DialOpt
に渡すと、常に同じ絶対時刻がデッドラインとして使用され、期待される相対的なタイムアウト動作が得られませんでした。- 非効率なオプション処理:
DialOpt
関数内で、netFromOptions
、deadlineFromOptions
、localAddrFromOptions
といった複数の関数が、それぞれ独立してopts []DialOption
スライスを走査していました。これは冗長であり、オプションの数が増えるほどパフォーマンスが低下する可能性がありました。 DialOption
インターフェースの限定的な役割: 以前のDialOption
インターフェースは、dialOption()
という空のメソッドを持つだけで、主に型アサーションのためのマーカーインターフェースとして機能していました。オプションの具体的な設定ロジックは、DialOpt
関数内のヘルパー関数に分散していました。
変更後の解決策:
dialOpts
構造体の導入:dialOpts
という新しいプライベート構造体が導入されました。この構造体は、deadline
、localAddr
、network
、deferredConnect
といった、Dial
操作に必要なすべてのオプション値を保持します。これにより、すべてのオプションが単一の構造体に集約され、管理が容易になりました。DialOption
インターフェースの変更:DialOption
インターフェースは、setDialOpt(*dialOpts)
という新しいメソッドを持つように変更されました。これにより、DialOption
を実装する各型(dialNetwork
、dialDeadline
、dialTimeoutOpt
、tcpFastOpen
、localAddrOption
)は、自身のsetDialOpt
メソッド内で、渡されたdialOpts
構造体の対応するフィールドを設定する責任を持つようになりました。Timeout
オプションのデッドライン動的評価:Timeout(d time.Duration)
関数は、dialTimeoutOpt
という新しい型を返すようになりました。このdialTimeoutOpt
型のsetDialOpt
メソッド内で、time.Now().Add(time.Duration(d))
が呼び出されます。これにより、Timeout
オプションがDialOpt
に渡され、setDialOpt
が実行されるたびに、その時点の現在時刻に基づいてデッドラインが計算されるようになり、タイムアウトの再利用問題が解決されました。- 単一パスでのオプション処理:
DialOpt
関数は、opts ...DialOption
スライスを一度だけループし、各DialOption
のsetDialOpt
メソッドを呼び出すようになりました。これにより、すべてのオプションが効率的にdialOpts
構造体に集約され、冗長な複数回のスライス走査がなくなりました。
この変更により、Dial
オプションの処理がより堅牢で、効率的で、拡張性の高いものになりました。各オプションが自身の設定ロジックを持つことで、コードのモジュール性が向上し、将来的に新しいDialOption
を追加する際も、DialOpt
関数の内部ロジックを変更する必要がなくなりました。
コアとなるコードの変更箇所
変更は主に src/pkg/net/dial.go
ファイルに集中しています。
-
DialOption
インターフェースの変更:--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -11,7 +11,27 @@ import ( // A DialOption modifies a DialOpt call. type DialOption interface { - dialOption() + setDialOpt(*dialOpts) }
dialOption()
からsetDialOpt(*dialOpts)
へ変更。 -
dialOpts
構造体の新規追加:--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -11,7 +11,27 @@ import ( // A DialOption modifies a DialOpt call. type DialOption interface { - dialOption() + setDialOpt(*dialOpts) } + +var noLocalAddr Addr // nil + +// dialOpts holds all the dial options, populated by a DialOption's +// setDialOpt. +// +// All fields may be their zero value. +type dialOpts struct { + deadline time.Time + localAddr Addr + network string // if empty, "tcp" + deferredConnect bool +} + +func (o *dialOpts) net() string { + if o.network == "" { + return "tcp" + } + return o.network }
dialOpts
構造体と、そのnet()
メソッドが追加されました。 -
各
DialOption
実装のsetDialOpt
メソッドへの変更:dialNetwork
--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -38,7 +58,9 @@ func Network(net string) DialOption { type dialNetwork string -func (dialNetwork) dialOption() {} +func (s dialNetwork) setDialOpt(o *dialOpts) { + o.network = string(s) +}
dialDeadline
(型定義も変更)--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -46,19 +68,29 @@ func Deadline(t time.Time) DialOption { return dialDeadline(t) } +type dialDeadline time.Time + +func (t dialDeadline) setDialOpt(o *dialOpts) { + o.deadline = time.Time(t) +} + // Timeout returns a DialOption to fail a dial that doesn't // complete within the provided duration. func Timeout(d time.Duration) DialOption { - return dialDeadline(time.Now().Add(d)) + return dialTimeoutOpt(d) } -type dialDeadline time.Time +type dialTimeoutOpt time.Duration -func (dialDeadline) dialOption() {} +func (d dialTimeoutOpt) setDialOpt(o *dialOpts) { + o.deadline = time.Now().Add(time.Duration(d)) +}
tcpFastOpen
--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -74,7 +106,9 @@ type localAddrOption struct { la Addr } -func (localAddrOption) dialOption() {} +func (a localAddrOption) setDialOpt(o *dialOpts) { + o.localAddr = a.la +}
localAddrOption
--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -74,7 +106,9 @@ type localAddrOption struct { la Addr } -func (localAddrOption) dialOption() {} +func (a localAddrOption) setDialOpt(o *dialOpts) { + o.localAddr = a.la +}
-
DialOpt
関数のロジック変更:--- a/src/pkg/net/dial.go +++ b/src/pkg/net/dial.go @@ -155,47 +189,19 @@ func Dial(net, addr string) (Conn, error) { return DialOpt(addr, dialNetwork(net))\n }\n \n-func netFromOptions(opts []DialOption) string {\n-\tfor _, opt := range opts {\n-\t\tif p, ok := opt.(dialNetwork); ok {\n-\t\t\treturn string(p)\n-\t\t}\n-\t}\n-\treturn \"tcp\"\n-}\n-\n-func deadlineFromOptions(opts []DialOption) time.Time {\n-\tfor _, opt := range opts {\n-\t\tif d, ok := opt.(dialDeadline); ok {\n-\t\t\treturn time.Time(d)\n-\t\t}\n-\t}\n-\treturn noDeadline\n-}\n-\n-var noLocalAddr Addr // nil\n-\n-func localAddrFromOptions(opts []DialOption) Addr {\n-\tfor _, opt := range opts {\n-\t\tif o, ok := opt.(localAddrOption); ok {\n-\t\t\treturn o.la\n-\t\t}\n-\t}\n-\treturn noLocalAddr\n-}\n-\n // DialOpt dials addr using the provided options.\n // If no options are provided, DialOpt(addr) is equivalent\n // to Dial("tcp", addr). See Dial for the syntax of addr.\n func DialOpt(addr string, opts ...DialOption) (Conn, error) {\n-\tnet := netFromOptions(opts)\n-\tdeadline := deadlineFromOptions(opts)\n-\tla := localAddrFromOptions(opts)\n-\tra, err := resolveAddr("dial", net, addr, deadline)\n+\tvar o dialOpts\n+\tfor _, opt := range opts {\n+\t\topt.setDialOpt(&o)\n+\t}\n+\tra, err := resolveAddr("dial", o.net(), addr, o.deadline)\n \tif err != nil {\n \t\treturn nil, err\n \t}\n-\treturn dial(net, addr, la, ra, deadline)\n+\treturn dial(o.net(), addr, o.localAddr, ra, o.deadline)\n }\n \n func dial(net, addr string, la, ra Addr, deadline time.Time) (c Conn, err error) {\n``` `netFromOptions`などのヘルパー関数が削除され、`dialOpts`構造体と`setDialOpt`メソッドを使った単一ループ処理に置き換えられました。
コアとなるコードの解説
このコミットの核心は、DialOption
インターフェースの役割の変更と、dialOpts
という新しい構造体の導入にあります。
-
DialOption
インターフェースの変更:- 変更前:
type DialOption interface { dialOption() }
これは、DialOption
を実装する型がdialOption()
という空のメソッドを持つことを要求するだけの「マーカーインターフェース」でした。このインターフェース自体は、オプションの具体的な動作を定義していませんでした。オプションの抽出ロジックは、DialOpt
関数内のnetFromOptions
などのヘルパー関数に分散していました。 - 変更後:
type DialOption interface { setDialOpt(*dialOpts) }
この変更により、DialOption
インターフェースは、dialOpts
構造体へのポインタを受け取り、その構造体のフィールドを設定するsetDialOpt
メソッドを持つことを要求するようになりました。これにより、各DialOption
の実装が、自身の種類に応じたオプション値をdialOpts
構造体に「設定する」という具体的な責任を持つようになりました。これは、Goのインターフェースが持つポリモーフィズムの強力な活用例です。
- 変更前:
-
dialOpts
構造体の導入:type dialOpts struct { deadline time.Time; localAddr Addr; network string; deferredConnect bool }
この新しいプライベート構造体は、Dial
操作に必要なすべての設定(デッドライン、ローカルアドレス、ネットワークタイプ、遅延接続フラグなど)を一元的に保持します。これにより、オプションが複数の変数に散らばることなく、単一のコンテキストで管理されるようになります。
-
Timeout
オプションのデッドライン動的評価:- 変更前:
func Timeout(d time.Duration) DialOption { return dialDeadline(time.Now().Add(d)) }
Timeout
オプションが作成された時点でtime.Now()
が呼び出され、デッドラインが固定されていました。 - 変更後:
func Timeout(d time.Duration) DialOption { return dialTimeoutOpt(d) }
とfunc (d dialTimeoutOpt) setDialOpt(o *dialOpts) { o.deadline = time.Now().Add(time.Duration(d)) }
Timeout
関数は、time.Duration
をラップしたdialTimeoutOpt
型を返します。このdialTimeoutOpt
型のsetDialOpt
メソッドが、DialOpt
関数内で呼び出される際に、その時点のtime.Now()
を使用してデッドラインを計算し、dialOpts
構造体のdeadline
フィールドに設定します。これにより、Timeout
オプションを何度再利用しても、常にDial
呼び出し時点からの相対的なタイムアウトが適用されるようになりました。
- 変更前:
-
DialOpt
関数のリファクタリング:- 変更前:
DialOpt
関数は、netFromOptions
、deadlineFromOptions
、localAddrFromOptions
といった複数のヘルパー関数を呼び出し、それぞれがopts []DialOption
スライスを個別に走査していました。 - 変更後:
DialOpt
関数は、var o dialOpts
でdialOpts
構造体のゼロ値を初期化し、for _, opt := range opts { opt.setDialOpt(&o) }
という単一のループで、渡されたすべてのDialOption
のsetDialOpt
メソッドを呼び出します。これにより、各オプションがo
(dialOpts
インスタンス)に必要な設定を直接書き込みます。ループが完了すると、o
にはすべてのオプションが適用された最終的な設定が格納され、これがresolveAddr
やdial
関数に渡されます。
- 変更前:
この一連の変更により、Dial
オプションの処理は、より効率的で、柔軟性があり、将来の拡張にも対応しやすい設計になりました。各オプションが自身の設定ロジックを持つことで、コードの関心事が分離され、保守性も向上しています。
関連リンク
- https://golang.org/cl/7608045 (Go Code Review - CL 7608045)
参考にした情報源リンク
- 特になし (コミット情報とGo言語の一般的な知識に基づいて解説を生成しました。)