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

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

このコミットは、Go言語の標準ライブラリ net/smtp パッケージにおける重要な改善を含んでいます。具体的には、SendMail 関数がSMTP接続を適切に閉じるように修正され、Client 型に明示的な Close メソッドが追加されました。これにより、リソースリークの防止とAPIの使いやすさが向上しています。

コミット

commit 83db738786704e6e93434f4c73e285383df2342b
Author: Alex Jin <toalexjin@gmail.com>
Date:   Mon Jun 17 16:53:27 2013 -0700

    net/smtp: close conn in SendMail; add Client.Close method
    
    R=rsc, dave, bradfitz
    CC=golang-dev
    https://golang.org/cl/10082044
---
 src/pkg/net/smtp/smtp.go      | 11 +++++++++--
 src/pkg/net/smtp/smtp_test.go |  3 +++
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/pkg/net/smtp/smtp.go b/src/pkg/net/smtp/smtp.go
index 4b91778770..dc7e1ceb8f 100644
--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -41,12 +41,13 @@ type Client struct {
 }
 
 // Dial returns a new Client connected to an SMTP server at addr.
+// The addr must include a port number.
 func Dial(addr string) (*Client, error) {
 	conn, err := net.Dial("tcp", addr)
 	if err != nil {
 		return nil, err
 	}
-\thost := addr[:strings.Index(addr, ":")]
+\thost, _, _ := net.SplitHostPort(addr)
 	return NewClient(conn, host)
 }
 
@@ -63,6 +64,11 @@ func NewClient(conn net.Conn, host string) (*Client, error) {
 	return c, nil
 }
 
+// Close closes the connection.
+func (c *Client) Close() error {
+\treturn c.Text.Close()\n}
+\n // hello runs a hello exchange if needed.
 func (c *Client) hello() error {\n \tif !c.didHello {\n@@ -264,7 +270,8 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {\n \tif err != nil {\n \t\treturn err\n \t}\n-\tif err := c.hello(); err != nil {\n+\tdefer c.Close()\n+\tif err = c.hello(); err != nil {\n \t\treturn err\n \t}\n \tif ok, _ := c.Extension("STARTTLS"); ok {\ndiff --git a/src/pkg/net/smtp/smtp_test.go b/src/pkg/net/smtp/smtp_test.go
index c190b32c05..b696dbe3cb 100644
--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -238,6 +238,7 @@ func TestNewClient(t *testing.T) {\n \tif err != nil {\n \t\tt.Fatalf("NewClient: %v\\n(after %v)\", err, out())\n \t}\n+\tdefer c.Close()\n \tif ok, args := c.Extension("aUtH\"); !ok || args != "LOGIN PLAIN" {\n \t\tt.Fatalf("Expected AUTH supported")\n \t}\n@@ -278,6 +279,7 @@ func TestNewClient2(t *testing.T) {\n \tif err != nil {\n \t\tt.Fatalf("NewClient: %v\", err)\n \t}\n+\tdefer c.Close()\n \tif ok, _ := c.Extension("DSN\"); ok {\n \t\tt.Fatalf("Shouldn't support DSN")\n \t}\n@@ -323,6 +325,7 @@ func TestHello(t *testing.T) {\n \t\tif err != nil {\n \t\t\tt.Fatalf("NewClient: %v\", err)\n \t\t}\n+\t\tdefer c.Close()\n \t\tc.localName = "customhost"\n \t\terr = nil\n \n```

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

[https://github.com/golang/go/commit/83db738786704e6e93434f4c73e285383df2342b](https://github.com/golang/go/commit/83db738786704e6e93434f4c73e285383df2342b)

## 元コミット内容

このコミットの元の意図は、`net/smtp` パッケージにおいて、`SendMail` 関数がSMTPサーバーとの接続を適切に閉じるようにすること、そして `Client` 型に明示的な `Close` メソッドを追加することです。これにより、リソースの適切な解放が保証され、接続リークを防ぐことができます。

## 変更の背景

Go言語の `net/smtp` パッケージは、SMTP(Simple Mail Transfer Protocol)を使用してメールを送信するためのクライアント機能を提供します。SMTPはTCP/IP上で動作するプロトコルであり、サーバーとの間で永続的な接続を確立し、コマンドと応答をやり取りします。

このコミットが行われる以前の `net/smtp` パッケージの `SendMail` 関数には、メール送信後にSMTPサーバーとの接続を明示的に閉じないという問題がありました。これは、`SendMail` 関数が内部で `net.Dial` を使用して新しいTCP接続を確立するにもかかわらず、その接続を適切にクリーンアップしていなかったことを意味します。結果として、`SendMail` が呼び出されるたびに新しい接続が確立され、それが閉じられないまま放置されるため、サーバー側のリソースが枯渇したり、クライアント側でファイルディスクリプタがリークしたりする可能性がありました。

また、`Client` 型には接続を閉じるためのパブリックなメソッドが存在しませんでした。これにより、`Client` オブジェクトを直接操作してメールを送信するような高度なシナリオにおいて、開発者が接続を明示的に管理し、リソースを解放することが困難でした。

これらの問題を解決し、`net/smtp` パッケージの堅牢性と使いやすさを向上させるために、このコミットが導入されました。

## 前提知識の解説

### SMTP (Simple Mail Transfer Protocol)

SMTPは、電子メールを送信するための標準的なインターネットプロトコルです。クライアント(メール送信者)とサーバー(メールサーバー)の間でメッセージを交換するために使用されます。SMTPは通常、TCPポート25(暗号化なし)、465(SMTPS)、または587(Submission)を使用します。

### TCP接続とリソース管理

TCP(Transmission Control Protocol)は、インターネット上で信頼性の高いデータ転送を提供するプロトコルです。TCP接続は、クライアントとサーバーの間で確立される仮想的な通信チャネルであり、データの送受信が完了したら明示的に閉じられる必要があります。接続を閉じないと、サーバー側ではその接続を維持するためのリソース(メモリ、ファイルディスクリプタなど)が解放されず、リソースリークにつながります。

### Go言語の `defer` キーワード

Go言語の `defer` ステートメントは、それが含まれる関数がリターンする直前に、指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除、ネットワーク接続のクローズなど)を確実に行うために非常に便利です。`defer` はLIFO(Last-In, First-Out)順で実行されます。

### `net.Dial`

Go言語の `net` パッケージは、ネットワークI/Oのプリミティブを提供します。`net.Dial(network, address string)` 関数は、指定されたネットワークアドレスへの接続を確立します。例えば、`net.Dial("tcp", "example.com:25")` は、`example.com` のポート25にTCP接続を試みます。成功すると、`net.Conn` インターフェースを実装するオブジェクトを返します。この `net.Conn` オブジェクトは、データの読み書きと接続のクローズを行うためのメソッド(`Close()` など)を提供します。

### `net.SplitHostPort`

`net.SplitHostPort(hostport string)` 関数は、ネットワークアドレス文字列(例: "localhost:8080", "[::1]:80")をホストとポートの2つの部分に分割します。これは、アドレスからホスト名やIPアドレスを抽出し、ポート番号を分離する際に便利です。

## 技術的詳細

このコミットの技術的な詳細を掘り下げます。

1.  **`Client.Close()` メソッドの追加**:
    `net/smtp` パッケージの `Client` 型は、SMTPサーバーとの接続を管理します。以前は、この `Client` 型には接続を明示的に閉じるためのパブリックなメソッドがありませんでした。このコミットでは、`Client` 型に `Close() error` メソッドが追加されました。
    ```go
    // Close closes the connection.
    func (c *Client) Close() error {
        return c.Text.Close()
    }
    ```
    このメソッドは、内部で `c.Text.Close()` を呼び出しています。`c.Text` は `textproto.Conn` 型であり、これは基となる `net.Conn` をラップしてテキストベースのプロトコル(SMTPなど)の読み書きを容易にするものです。`textproto.Conn` も `Close()` メソッドを持っており、これが最終的に基となるTCP接続を閉じます。この変更により、`Client` オブジェクトの利用者は、不要になったSMTP接続を明示的に解放できるようになりました。

2.  **`SendMail` 関数での `defer c.Close()` の導入**:
    `SendMail` 関数は、SMTPサーバーへの接続、認証、メールの送信といった一連の処理をカプセル化する高レベルな関数です。この関数は内部で `Dial` を呼び出して新しい `Client` オブジェクトを作成します。
    変更前は、`SendMail` 関数内で作成された `Client` オブジェクトの接続が、関数終了時に自動的に閉じられることはありませんでした。
    変更後、`SendMail` 関数に以下の行が追加されました。
    ```go
    defer c.Close()
    ```
    この `defer` ステートメントにより、`SendMail` 関数が正常に完了した場合でも、エラーが発生して途中でリターンした場合でも、必ず `c.Close()` が呼び出され、SMTP接続が閉じられることが保証されます。これにより、`SendMail` の呼び出し元が接続のクローズを意識する必要がなくなり、リソースリークが効果的に防止されます。

3.  **`Dial` 関数での `net.SplitHostPort` の使用**:
    `Dial` 関数は、与えられたアドレス文字列からホスト名を抽出し、`NewClient` に渡す役割を担っています。
    変更前は、`addr[:strings.Index(addr, ":")]` という方法でホスト名を抽出していました。これは、アドレス文字列にポート番号が含まれていない場合や、IPv6アドレスのようにコロンが複数含まれる場合に問題を引き起こす可能性がありました。
    変更後、より堅牢な `net.SplitHostPort(addr)` が使用されるようになりました。
    ```go
    // The addr must include a port number.
    func Dial(addr string) (*Client, error) {
        conn, err := net.Dial("tcp", addr)
        if err != nil {
            return nil, err
        }
        host, _, _ := net.SplitHostPort(addr) // 変更点
        return NewClient(conn, host)
    }
    ```
    `net.SplitHostPort` は、アドレス文字列をホストとポートに安全に分割します。これにより、IPv6アドレスを含む複雑なアドレス形式にも対応できるようになり、ホスト名の抽出がより正確かつ堅牢になりました。また、`Dial` 関数のコメントに「The addr must include a port number.」が追加され、引数の要件が明確化されました。

4.  **テストコードでの `defer c.Close()` の追加**:
    `src/pkg/net/smtp/smtp_test.go` 内の複数のテスト関数(`TestNewClient`, `TestNewClient2`, `TestHello` など)にも、`defer c.Close()` が追加されました。これは、テストが終了した後に確立されたSMTP接続が確実に閉じられるようにするためです。テストコードにおいてもリソースリークを防ぎ、テストの独立性と信頼性を高める上で重要です。

これらの変更は、`net/smtp` パッケージの堅牢性、信頼性、および使いやすさを大幅に向上させるものです。特に、`SendMail` 関数が自動的に接続を閉じるようになったことで、一般的なメール送信シナリオでのリソース管理が簡素化されました。

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

### `src/pkg/net/smtp/smtp.go`

```diff
--- a/src/pkg/net/smtp/smtp.go
+++ b/src/pkg/net/smtp/smtp.go
@@ -41,12 +41,13 @@ type Client struct {
 }
 
 // Dial returns a new Client connected to an SMTP server at addr.
+// The addr must include a port number.
 func Dial(addr string) (*Client, error) {
 	conn, err := net.Dial("tcp", addr)
 	if err != nil {
 		return nil, err
 	}
-\thost := addr[:strings.Index(addr, ":")]
+\thost, _, _ := net.SplitHostPort(addr)
 	return NewClient(conn, host)
 }
 
@@ -63,6 +64,11 @@ func NewClient(conn net.Conn, host) (*Client, error) {
 	return c, nil
 }
 
+// Close closes the connection.
+func (c *Client) Close() error {
+\treturn c.Text.Close()
+}
+\n // hello runs a hello exchange if needed.
 func (c *Client) hello() error {\n \tif !c.didHello {\n@@ -264,7 +270,8 @@ func SendMail(addr string, a Auth, from string, to []string, msg []byte) error {\n \tif err != nil {\n \t\treturn err\n \t}\n-\tif err := c.hello(); err != nil {\n+\tdefer c.Close()\n+\tif err = c.hello(); err != nil {\n \t\treturn err\n \t}\n \tif ok, _ := c.Extension("STARTTLS"); ok {

src/pkg/net/smtp/smtp_test.go

--- a/src/pkg/net/smtp/smtp_test.go
+++ b/src/pkg/net/smtp/smtp_test.go
@@ -238,6 +238,7 @@ func TestNewClient(t *testing.T) {
 	if err != nil {
 		t.Fatalf("NewClient: %v\n(after %v)", err, out())
 	}
+\tdefer c.Close()
 	if ok, args := c.Extension("aUtH"); !ok || args != "LOGIN PLAIN" {
 		t.Fatalf("Expected AUTH supported")
 	}
@@ -278,6 +279,7 @@ func TestNewClient2(t *testing.T) {
 	if err != nil {
 		t.Fatalf("NewClient: %v", err)
 	}
+\tdefer c.Close()
 	if ok, _ := c.Extension("DSN"); ok {
 		t.Fatalf("Shouldn't support DSN")
 	}
@@ -323,6 +325,7 @@ func TestHello(t *testing.T) {
 		if err != nil {
 			t.Fatalf("NewClient: %v", err)
 		}
+\t\tdefer c.Close()
 		c.localName = "customhost"
 		err = nil
 

コアとなるコードの解説

src/pkg/net/smtp/smtp.go の変更点

  1. Dial 関数内のホスト名抽出の改善:

    • 変更前: host := addr[:strings.Index(addr, ":")] このコードは、アドレス文字列 addr の最初のコロン : までの部分をホスト名として抽出していました。これは単純な host:port 形式には対応できますが、IPv6アドレス(例: [::1]:25)のようにホスト名自体にコロンが含まれる場合や、ポート番号がない場合に誤動作する可能性がありました。
    • 変更後: host, _, _ := net.SplitHostPort(addr) net.SplitHostPort 関数は、host:port 形式のアドレス文字列を安全にホストとポートに分割します。これにより、IPv6アドレスを含むより複雑なアドレス形式にも正しく対応できるようになり、ホスト名の抽出がより堅牢になりました。返される3つの値のうち、2番目と3番目の戻り値(ポートとエラー)はここでは不要なため、_ で破棄しています。
    • コメントの追加: // The addr must include a port number. Dial 関数の引数 addr がポート番号を含む必要があるという制約が明示されました。
  2. Client.Close() メソッドの追加:

    • Client 型に Close() error メソッドが追加されました。
    • このメソッドは c.Text.Close() を呼び出します。c.Texttextproto.Conn 型であり、その Close() メソッドが基となるネットワーク接続(net.Conn)を閉じます。
    • これにより、Client オブジェクトを直接扱うユーザーが、SMTP接続を明示的に終了させることができるようになりました。
  3. SendMail 関数での defer c.Close() の導入:

    • SendMail 関数内で Client オブジェクト c が作成された直後に defer c.Close() が追加されました。
    • defer ステートメントは、その関数(この場合は SendMail)が終了する直前に c.Close() が実行されることを保証します。これにより、SendMail 関数が正常に完了した場合でも、途中でエラーが発生してリターンした場合でも、SMTP接続が確実に閉じられ、リソースリークが防止されます。
    • if err := c.hello(); err != nil { の行が if err = c.hello(); err != nil { に変更されていますが、これは defer の追加とは直接関係なく、err 変数の再宣言を避けるための慣用的なGoの書き方です。

src/pkg/net/smtp/smtp_test.go の変更点

  • TestNewClient, TestNewClient2, TestHello などのテスト関数内で Client オブブジェクトが作成された後に defer c.Close() が追加されました。
  • これにより、各テストケースが終了する際に、そのテストで確立されたSMTP接続が確実に閉じられるようになります。これは、テストの独立性を保ち、テスト実行後のリソースクリーンアップを確実に行うために重要です。

これらの変更は、net/smtp パッケージの堅牢性とリソース管理の改善に大きく貢献しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (net/smtp, net, defer に関するセクション)
  • SMTPプロトコルに関する一般的な情報源 (RFC 5321など)
  • TCP/IPネットワークプログラミングに関する一般的な知識
  • Go言語におけるリソース管理とエラーハンドリングのベストプラクティスに関する記事