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

[インデックス 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オプションの再利用に関する問題の修正です。

  1. タイムアウトデッドラインの固定化: 以前の実装では、Timeoutオプションがtime.Now().Add(d)のように、オプションが作成された時点の現在時刻に基づいてデッドラインを計算していました。このため、一度作成されたTimeoutオプションを複数のDial呼び出しで再利用すると、常に最初のDial呼び出し時に計算されたデッドラインが適用されてしまい、後続の呼び出しでは期待通りの相対的なタイムアウトではなく、過去の固定されたデッドラインが使われるという問題がありました。これは、特に接続が遅延した場合に、ユーザーが意図したよりも早くタイムアウトが発生したり、逆にタイムアウトしなかったりする原因となっていました。

  2. オプション処理の効率化とインターフェースの改善: 以前のDialOpt関数は、netFromOptionsdeadlineFromOptionslocalAddrFromOptionsといった複数のヘルパー関数を呼び出し、それぞれが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.Timetime.Duration:
    • time.Time: 特定の時点を表すGoの型です。ネットワーク操作におけるデッドライン(期限)を設定する際に使用されます。
    • time.Duration: 時間の長さを表すGoの型です。タイムアウト期間を指定する際に使用されます。
  • ネットワークタイムアウトとデッドライン:
    • タイムアウト (Timeout): ある操作が完了するまでの最大許容時間です。この時間を超えると操作は中断され、エラーが返されます。time.Durationで指定されます。
    • デッドライン (Deadline): ある操作が完了しなければならない絶対的な時刻です。time.Timeで指定されます。タイムアウトは相対的な期間であるのに対し、デッドラインは絶対的な時点です。
  • 構造体とメソッド: Go言語におけるデータ構造(構造体)と、それに紐付けられた関数(メソッド)の概念です。このコミットでは、dialOptsという新しい構造体が導入され、各DialOptionの実装がこの構造体のフィールドを設定するメソッドを持つように変更されています。
  • インターフェースとポリモーフィズム: DialOptionインターフェースは、異なる種類のオプション(ネットワークタイプ、デッドライン、ローカルアドレスなど)を統一的に扱うためのGoの機能です。各オプションはDialOptionインターフェースを実装することで、DialOpt関数に渡され、それぞれのsetDialOptメソッドが呼び出されることで、dialOpts構造体に適切な値が設定されます。

技術的詳細

このコミットは、netパッケージのDialオプション処理を根本的に改善しています。

変更前の問題点:

  • Timeoutオプションのデッドライン固定化: Timeout(d time.Duration)関数は、time.Now().Add(d)を使用してデッドラインを計算し、dialDeadline型として返していました。このdialDeadlinetime.Timeをラップしたものであり、一度作成されるとデッドラインの値は固定されていました。そのため、同じTimeoutオプションインスタンスを複数回DialOptに渡すと、常に同じ絶対時刻がデッドラインとして使用され、期待される相対的なタイムアウト動作が得られませんでした。
  • 非効率なオプション処理: DialOpt関数内で、netFromOptionsdeadlineFromOptionslocalAddrFromOptionsといった複数の関数が、それぞれ独立してopts []DialOptionスライスを走査していました。これは冗長であり、オプションの数が増えるほどパフォーマンスが低下する可能性がありました。
  • DialOptionインターフェースの限定的な役割: 以前のDialOptionインターフェースは、dialOption()という空のメソッドを持つだけで、主に型アサーションのためのマーカーインターフェースとして機能していました。オプションの具体的な設定ロジックは、DialOpt関数内のヘルパー関数に分散していました。

変更後の解決策:

  • dialOpts構造体の導入: dialOptsという新しいプライベート構造体が導入されました。この構造体は、deadlinelocalAddrnetworkdeferredConnectといった、Dial操作に必要なすべてのオプション値を保持します。これにより、すべてのオプションが単一の構造体に集約され、管理が容易になりました。
  • DialOptionインターフェースの変更: DialOptionインターフェースは、setDialOpt(*dialOpts)という新しいメソッドを持つように変更されました。これにより、DialOptionを実装する各型(dialNetworkdialDeadlinedialTimeoutOpttcpFastOpenlocalAddrOption)は、自身のsetDialOptメソッド内で、渡されたdialOpts構造体の対応するフィールドを設定する責任を持つようになりました。
  • Timeoutオプションのデッドライン動的評価: Timeout(d time.Duration)関数は、dialTimeoutOptという新しい型を返すようになりました。このdialTimeoutOpt型のsetDialOptメソッド内で、time.Now().Add(time.Duration(d))が呼び出されます。これにより、TimeoutオプションがDialOptに渡され、setDialOptが実行されるたびに、その時点の現在時刻に基づいてデッドラインが計算されるようになり、タイムアウトの再利用問題が解決されました。
  • 単一パスでのオプション処理: DialOpt関数は、opts ...DialOptionスライスを一度だけループし、各DialOptionsetDialOptメソッドを呼び出すようになりました。これにより、すべてのオプションが効率的にdialOpts構造体に集約され、冗長な複数回のスライス走査がなくなりました。

この変更により、Dialオプションの処理がより堅牢で、効率的で、拡張性の高いものになりました。各オプションが自身の設定ロジックを持つことで、コードのモジュール性が向上し、将来的に新しいDialOptionを追加する際も、DialOpt関数の内部ロジックを変更する必要がなくなりました。

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

変更は主に src/pkg/net/dial.go ファイルに集中しています。

  1. 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)へ変更。

  2. 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()メソッドが追加されました。

  3. 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
      +}
      
  4. 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という新しい構造体の導入にあります。

  1. DialOptionインターフェースの変更:

    • 変更前: type DialOption interface { dialOption() } これは、DialOptionを実装する型がdialOption()という空のメソッドを持つことを要求するだけの「マーカーインターフェース」でした。このインターフェース自体は、オプションの具体的な動作を定義していませんでした。オプションの抽出ロジックは、DialOpt関数内のnetFromOptionsなどのヘルパー関数に分散していました。
    • 変更後: type DialOption interface { setDialOpt(*dialOpts) } この変更により、DialOptionインターフェースは、dialOpts構造体へのポインタを受け取り、その構造体のフィールドを設定するsetDialOptメソッドを持つことを要求するようになりました。これにより、各DialOptionの実装が、自身の種類に応じたオプション値をdialOpts構造体に「設定する」という具体的な責任を持つようになりました。これは、Goのインターフェースが持つポリモーフィズムの強力な活用例です。
  2. dialOpts構造体の導入:

    • type dialOpts struct { deadline time.Time; localAddr Addr; network string; deferredConnect bool } この新しいプライベート構造体は、Dial操作に必要なすべての設定(デッドライン、ローカルアドレス、ネットワークタイプ、遅延接続フラグなど)を一元的に保持します。これにより、オプションが複数の変数に散らばることなく、単一のコンテキストで管理されるようになります。
  3. 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呼び出し時点からの相対的なタイムアウトが適用されるようになりました。
  4. DialOpt関数のリファクタリング:

    • 変更前: DialOpt関数は、netFromOptionsdeadlineFromOptionslocalAddrFromOptionsといった複数のヘルパー関数を呼び出し、それぞれがopts []DialOptionスライスを個別に走査していました。
    • 変更後: DialOpt関数は、var o dialOptsdialOpts構造体のゼロ値を初期化し、for _, opt := range opts { opt.setDialOpt(&o) }という単一のループで、渡されたすべてのDialOptionsetDialOptメソッドを呼び出します。これにより、各オプションがodialOptsインスタンス)に必要な設定を直接書き込みます。ループが完了すると、oにはすべてのオプションが適用された最終的な設定が格納され、これがresolveAddrdial関数に渡されます。

この一連の変更により、Dialオプションの処理は、より効率的で、柔軟性があり、将来の拡張にも対応しやすい設計になりました。各オプションが自身の設定ロジックを持つことで、コードの関心事が分離され、保守性も向上しています。

関連リンク

参考にした情報源リンク

  • 特になし (コミット情報とGo言語の一般的な知識に基づいて解説を生成しました。)