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

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

このコミットは、Go言語の標準ライブラリ net/http パッケージ内の triv.go ファイルに対する変更です。triv.go は、net/http パッケージの基本的な機能を示すためのトリビアルな(単純な)サンプルコードやテストコードとして機能していると考えられます。具体的には、HTTPリクエストを処理し、外部コマンド(/bin/date)を実行してその出力をHTTPレスポンスとして返す機能、およびログ出力を行うHTTPハンドラが含まれています。

コミット

commit 1c224ab9dd1833f4548a49d40d2bb0a264a74767
Author: Robert Hencke <robert.hencke@gmail.com>
Date:   Wed Mar 14 20:25:57 2012 -0700

    net/http: ensure triv.go compiles and runs
    
    R=golang-dev, bradfitz, dsymonds, dave, r
    CC=golang-dev
    https://golang.org/cl/5795069

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

https://github.com/golang/go/commit/1c224ab9dd1833f4548a49d40d2bb0a264a74767

元コミット内容

net/http: ensure triv.go compiles and runs

R=golang-dev, bradfitz, dsymonds, dave, r
CC=golang-dev
https://golang.org/cl/5795069

変更の背景

このコミットの背景は、コミットメッセージにある通り「triv.go がコンパイルされ、実行されることを保証する」ことです。これは、既存のコードが何らかの理由で正しく動作しなくなっていたか、またはより堅牢でGoのイディオムに沿った方法で外部コマンドを実行する必要があったことを示唆しています。

元のコードでは、os.Pipeos.StartProcess を組み合わせて外部コマンドを実行し、その出力をHTTPレスポンスとして返していました。しかし、この方法はパイプの管理、プロセスの開始、エラーハンドリングが複雑になりがちです。特に、パイプのクローズ順序やプロセスの待機処理に不備があると、リソースリークやデッドロック、予期せぬエラーが発生する可能性があります。

この変更は、よりシンプルで安全な os/exec パッケージの exec.Command 関数を使用することで、これらの問題を解決し、コードの可読性と堅牢性を向上させることを目的としています。また、エラーハンドリングも http.Error を使用するように変更されており、HTTPアプリケーションにおける標準的なエラー応答の仕方に合わせています。

前提知識の解説

Go言語の標準ライブラリ

  • net/http パッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための主要なパッケージです。HTTPリクエストのルーティング、ハンドラの登録、レスポンスの書き込みなど、Webアプリケーション開発の基盤を提供します。

    • http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェースです。ヘッダーの設定やボディの書き込みに使用されます。
    • http.Request: 受信したHTTPリクエストの情報を保持する構造体です。URL、ヘッダー、ボディなどの情報にアクセスできます。
    • http.Error(w http.ResponseWriter, error string, code int): HTTPエラーレスポンスを生成し、指定されたステータスコードとエラーメッセージをクライアントに送信するためのヘルパー関数です。
  • os パッケージ: オペレーティングシステムと対話するための機能を提供します。ファイル操作、プロセス管理、環境変数へのアクセスなどが含まれます。

    • os.Pipe(): パイプを作成します。パイプは、プロセス間でデータをやり取りするための単方向の通信チャネルです。読み取り側と書き込み側の2つの *os.File を返します。
    • os.StartProcess(name string, argv []string, attr *ProcAttr): 新しいプロセスを開始します。name は実行するプログラムのパス、argv は引数、attr はプロセスの属性(標準入出力のリダイレクトなど)を指定します。この関数は低レベルであり、通常は os/exec パッケージが推奨されます。
    • os.ProcAttr: os.StartProcess に渡される構造体で、新しいプロセスの属性(環境変数、作業ディレクトリ、ファイルディスクリプタなど)を定義します。Files フィールドは、新しいプロセスの標準入出力(stdin, stdout, stderr)をリダイレクトするために使用されます。
  • os/exec パッケージ: 外部コマンドを実行するためのより高レベルなインターフェースを提供します。os.StartProcess よりも簡単に外部コマンドを実行し、その入出力を制御できます。

    • exec.Command(name string, arg ...string): 実行するコマンドと引数を表す *exec.Cmd 構造体を作成します。
    • (*exec.Cmd).Output(): コマンドを実行し、その標準出力(stdout)をバイトスライスとして返します。コマンドがエラーを返した場合、または標準エラー出力(stderr)がある場合は、エラーも返します。このメソッドは、コマンドの実行が完了するまでブロックします。

パイプとプロセス間通信

パイプは、Unix系システムにおけるプロセス間通信(IPC)の基本的なメカニズムの一つです。あるプロセスの標準出力が別のプロセスの標準入力に接続されることで、データがストリームとして流れます。os.Pipe() はGoプログラム内でこのパイプを作成し、プロセス間でデータをやり取りするために使用できます。

エラーハンドリング

Go言語では、エラーは戻り値として明示的に扱われます。関数がエラーを返す可能性がある場合、通常は最後の戻り値として error 型の値を返します。呼び出し元は、この error 値が nil でないかどうかをチェックすることで、エラーが発生したかどうかを判断します。

技術的詳細

このコミットの主要な変更点は、DateServer 関数における外部コマンドの実行方法です。

変更前:

func DateServer(rw http.ResponseWriter, req *http.Request) {
	rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
	r, w, err := os.Pipe()
	if err != nil {
		fmt.Fprintf(rw, "pipe: %s\n", err)
		return
	}

	p, err := os.StartProcess("/bin/date", []string{"date"}, &os.ProcAttr{Files: []*os.File{nil, w, w}})
	defer r.Close()
	w.Close()
	if err != nil {
		fmt.Fprintf(rw, "fork/exec: %s\n", err)
		return
	}
	io.Copy(rw, r)
	wait, err := p.Wait(0)
	if err != nil {
		fmt.Fprintf(rw, "wait: %s\n", err)
		return
	}
	if !wait.Exited() || wait.ExitStatus() != 0 {
		fmt.Fprintf(rw, "date: %v\n", wait)
		return
	}
}

変更前のコードでは、以下の手順で外部コマンドを実行していました。

  1. os.Pipe() を呼び出してパイプを作成します。r は読み取り側、w は書き込み側です。
  2. os.StartProcess() を使用して /bin/date コマンドを新しいプロセスとして開始します。
    • os.ProcAttr{Files: []*os.File{nil, w, w}} は、新しいプロセスの標準入力(nil)、標準出力(w)、標準エラー出力(w)を、作成したパイプの書き込み側にリダイレクトしています。これにより、/bin/date の出力がパイプに書き込まれます。
  3. defer r.Close() で読み取り側パイプを遅延クローズし、w.Close() で書き込み側パイプを即座にクローズします。w.Close()os.StartProcess の直後に行うことで、date コマンドが終了した際にパイプの書き込み側が閉じられ、io.Copy がEOF(End Of File)を受け取って終了できるようにします。
  4. io.Copy(rw, r) を使用して、パイプの読み取り側 r からHTTPレスポンスライター rw へデータをコピーします。これにより、/bin/date の出力がクライアントに送信されます。
  5. p.Wait(0) で子プロセスの終了を待ち、その終了ステータスを確認します。

このアプローチは低レベルであり、パイプの管理(特にクローズのタイミング)やエラーハンドリングが複雑になりがちです。例えば、w.Close() のタイミングが不適切だと、io.Copy がブロックし続ける可能性があります。また、エラーメッセージの出力も fmt.Fprintf を使って直接レスポンスボディに書き込んでおり、HTTPエラーコードを適切に設定していません。

変更後:

func DateServer(rw http.ResponseWriter, req *http.Request) {
	rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
	date, err := exec.Command("/bin/date").Output()
	if err != nil {
		http.Error(rw, err.Error(), 500)
		return
	}
	rw.Write(date)
}

変更後のコードでは、以下の手順で外部コマンドを実行しています。

  1. exec.Command("/bin/date").Output() を呼び出します。
    • exec.Command("/bin/date")/bin/date コマンドを実行するための *exec.Cmd オブジェクトを作成します。
    • .Output() メソッドは、コマンドを実行し、その標準出力(stdout)をバイトスライスとして返します。このメソッドは、コマンドが終了するまでブロックし、エラーが発生した場合は error を返します。
  2. エラーが発生した場合(err != nil)、http.Error(rw, err.Error(), 500) を使用してHTTP 500 Internal Server Error をクライアントに返します。これにより、HTTPの標準的なエラー応答メカニズムが利用されます。
  3. エラーがなければ、rw.Write(date) を使用して、取得した /bin/date の出力を直接HTTPレスポンスボディに書き込みます。

この変更により、コードは大幅に簡素化され、外部コマンドの実行とエラーハンドリングがよりGoのイディオムに沿った形になりました。os/exec パッケージは、プロセスの開始、パイプの管理、出力の収集といった複雑なタスクを抽象化してくれるため、開発者はより高レベルなロジックに集中できます。

また、Logger 関数も変更されています。

変更前:

func Logger(w http.ResponseWriter, req *http.Request) {
	log.Print(req.URL.Raw)
	w.WriteHeader(404)
	w.Write([]byte("oops"))
}

変更後:

func Logger(w http.ResponseWriter, req *http.Request) {
	log.Print(req.URL)
	http.Error(w, "oops", 404)
}

変更前は req.URL.Raw をログに出力し、w.WriteHeader(404)w.Write([]byte("oops")) を使って手動で404エラーを返していました。 変更後は req.URL をログに出力し、http.Error(w, "oops", 404) を使って標準的な方法で404エラーを返しています。req.URL.Raw はURLの生の文字列表現ですが、req.URL はパースされた *url.URL 構造体であり、より詳細な情報(Scheme, Host, Path, RawQueryなど)にアクセスできます。ログ出力の目的によっては req.URL の方が有用な場合があります。http.Error の使用は、DateServer の変更と同様に、HTTPエラー応答の標準化と簡素化を目的としています。

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

diff --git a/src/pkg/net/http/triv.go b/src/pkg/net/http/triv.go
index 269af0ca3d..adf5a00be1 100644
--- a/src/pkg/net/http/triv.go
+++ b/src/pkg/net/http/triv.go
@@ -15,6 +15,7 @@ import (
 	"log"
 	"net/http"
 	"os"
+	"os/exec"
 	"strconv"
 )
 
@@ -95,35 +96,18 @@ func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 // exec a program, redirecting output
 func DateServer(rw http.ResponseWriter, req *http.Request) {
 	rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
-	r, w, err := os.Pipe()
-	if err != nil {
-		fmt.Fprintf(rw, "pipe: %s\n", err)
-		return
-	}
- 
-	p, err := os.StartProcess("/bin/date", []string{"date"}, &os.ProcAttr{Files: []*os.File{nil, w, w}})
-	defer r.Close()
-	w.Close()
-	if err != nil {
-		fmt.Fprintf(rw, "fork/exec: %s\n", err)
-		return
-	}
-	io.Copy(rw, r)
-	wait, err := p.Wait(0)
+	date, err := exec.Command("/bin/date").Output()
 	if err != nil {
-		fmt.Fprintf(rw, "wait: %s\n", err)
+		http.Error(rw, err.Error(), 500)
 		return
 	}
-	if !wait.Exited() || wait.ExitStatus() != 0 {
-		fmt.Fprintf(rw, "date: %v\n", wait)
-		return
-	}
+	rw.Write(date)
 }
 
 func Logger(w http.ResponseWriter, req *http.Request) {
-	log.Print(req.URL.Raw)
-	w.WriteHeader(404)
-	w.Write([]byte("oops"))
+	log.Print(req.URL)
+	http.Error(w, "oops", 404)
 }
 
 var webroot = flag.String("root", "/home/rsc", "web root directory")

コアとなるコードの解説

import 文の変更

  • - os
  • + os/exec
    • os パッケージは引き続き使用されていますが、os/exec パッケージが新しくインポートされています。これは、外部コマンドの実行に os/exec パッケージの機能を使用するためです。

DateServer 関数の変更

  • 削除された行:

    • r, w, err := os.Pipe(): パイプの作成が不要になりました。
    • p, err := os.StartProcess(...): 低レベルなプロセス開始が不要になりました。
    • defer r.Close()w.Close(): パイプのクローズ処理が不要になりました。
    • io.Copy(rw, r): パイプからのコピーが不要になりました。
    • p.Wait(0) とその後のエラーチェック: プロセスの終了待機とステータスチェックが exec.Command().Output() に内包されました。
    • fmt.Fprintf(rw, ...): エラーメッセージの直接書き込みが http.Error に置き換えられました。
  • 追加・変更された行:

    • date, err := exec.Command("/bin/date").Output(): /bin/date コマンドを実行し、その標準出力を date 変数にバイトスライスとして格納します。エラーが発生した場合は err に格納されます。
    • if err != nil { http.Error(rw, err.Error(), 500); return }: コマンド実行中にエラーが発生した場合、http.Error を使用してHTTP 500 Internal Server Error をクライアントに返します。
    • rw.Write(date): コマンドの出力(date バイトスライス)をHTTPレスポンスボディに書き込みます。

この変更により、外部コマンドの実行ロジックが大幅に簡素化され、より安全でGoのイディオムに沿ったものになりました。

Logger 関数の変更

  • 削除された行:

    • log.Print(req.URL.Raw): ログ出力の対象が req.URL.Raw から req.URL に変更されました。
    • w.WriteHeader(404)w.Write([]byte("oops")): 手動でのHTTP 404エラー応答が http.Error に置き換えられました。
  • 追加・変更された行:

    • log.Print(req.URL): リクエストのURLオブジェクト全体をログに出力します。
    • http.Error(w, "oops", 404): HTTP 404 Not Found エラーをクライアントに返します。これにより、ヘッダーの設定とボディの書き込みが一度に行われます。

この変更も、HTTPエラー応答の標準化と簡素化を目的としています。

関連リンク

参考にした情報源リンク