[インデックス 10416] ファイルの概要
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh) におけるNameListのアンマーシャル処理に関するテストの修正と、exp/sshパッケージをpkg/Makefileに追加する変更を含んでいます。主な目的は、空のNameListが常に長さゼロの[]stringを返すようにすることです。
コミット
commit 00f9b7680a8481e988b20414699fb25b0030079b
Author: Dave Cheney <dave@cheney.net>
Date: Wed Nov 16 10:19:56 2011 -0500
exp/ssh: fix unmarshal test
Ensure that empty NameLists always return
a zero length []string, not nil.
In practice NameLists are only used in a few
message types and always consumed by a for
range function so the difference between nil
and []string{} is not significant.
Also, add exp/ssh to pkg/Makefile as suggested
by rsc.
R=rsc, agl
CC=golang-dev
https://golang.org/cl/5400042
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/00f9b7680a8481e988b20414699fb25b0030079b
元コミット内容
このコミットは、Go言語の実験的なSSHパッケージ (exp/ssh) において、NameListのアンマーシャル処理に関するテストを修正するものです。具体的には、空のNameListがnilではなく、常に長さゼロの[]string(空のスライス)を返すように変更します。
コミットメッセージでは、NameListが実際に使用される場面ではfor rangeループで消費されるため、nilと空のスライスの違いは実用上は重要ではないと述べられています。しかし、テストの観点からは、一貫性のある挙動が求められます。
また、このコミットには、rsc(おそらくRuss Cox)の提案により、exp/sshパッケージをpkg/Makefileに追加する変更も含まれています。
変更の背景
この変更の背景には、Go言語におけるスライスのnilと空のスライスの扱いの微妙な違いと、それらがテストやAPIの挙動に与える影響があります。
Go言語では、スライスがnilであることと、スライスが空であること(長さが0であること)は、異なる状態として扱われます。
nilスライス: 宣言されただけで初期化されていないスライスです。内部ポインタはnilで、長さも容量も0です。s == nilはtrueを返します。JSONマーシャリングではnullになります。- 空のスライス: 初期化されており、要素を一つも持たないスライスです。内部ポインタは
nilではありませんが、長さも容量も0です。s == nilはfalseを返します。JSONマーシャリングでは[](空の配列)になります。
多くのGoの組み込み関数(len(), cap(), append()など)やfor rangeループでは、nilスライスと空のスライスは同じように扱われます。しかし、テストにおいては、期待される出力がnilなのか、それとも空のスライスなのかによって、テストの合否が変わる可能性があります。
このコミットでは、exp/sshパッケージ内のNameListのアンマーシャル処理において、空の入力が与えられた場合にnilスライスが返されることが、テストの期待値と異なっていたと考えられます。そのため、テストを修正し、より堅牢な挙動を保証するために、空のNameListが常に空のスライスを返すように変更されました。
また、exp/sshパッケージがGoのビルドシステムに適切に組み込まれるように、pkg/Makefileへの追加も行われています。これは、新しいパッケージがGoの標準ライブラリの一部として認識され、ビルドプロセスに含まれるようにするための一般的な手順です。
前提知識の解説
Go言語におけるスライス (Slice)
Go言語のスライスは、配列をラップした動的なデータ構造です。スライスは、内部的にポインタ、長さ (length)、容量 (capacity) の3つの要素で構成されます。
- ポインタ: スライスが参照する基底配列の先頭要素へのポインタ。
- 長さ (length): スライスに含まれる要素の数。
len(s)で取得できます。 - 容量 (capacity): スライスの基底配列が保持できる要素の最大数。
cap(s)で取得できます。
スライスは、make関数やスライスリテラルを使って作成できます。
// nilスライス
var s1 []int // s1はnil、len(s1) == 0, cap(s1) == 0
// 空のスライス
s2 := []int{} // スライスリテラルで空のスライスを作成
s3 := make([]int, 0) // make関数で長さ0のスライスを作成
// s2, s3はnilではないが、len(s2) == 0, cap(s2) == 0
前述の通り、nilスライスと空のスライスは、len()やcap()の結果は同じですが、nilとの比較やJSONマーシャリングの挙動が異なります。この違いが、テストの期待値に影響を与えることがあります。
bytes.Split関数
bytes.Split関数は、Go言語のbytesパッケージに属する関数で、バイトスライスを特定のセパレータ(区切り文字)で分割するために使用されます。
func Split(s, sep []byte) [][]byte
s: 分割対象のバイトスライス。sep: 区切り文字として使用するバイトスライス。
この関数は、sをsepで分割し、結果として得られるバイトスライスのスライスを返します。
sepがs内に見つからない場合、s全体を含む単一のバイトスライスが返されます。sepが空のバイトスライス ([]byte{}) の場合、sの各バイトが個別のバイトスライスとして返されます。
このコミットでは、parseNameList関数内でbytes.Splitが使用されており、カンマ区切りの文字列をスライスに変換する際に利用されています。
exp/sshパッケージ
exp/sshは、Go言語の標準ライブラリの一部として提供されていた実験的なSSH(Secure Shell)プロトコル実装のパッケージです。expというプレフィックスは、そのパッケージがまだ実験段階であり、APIが安定していない可能性があることを示しています。SSHは、ネットワーク経由で安全にコンピュータを操作するためのプロトコルであり、認証、コマンド実行、ファイル転送などの機能を提供します。このパッケージは、SSHクライアントやサーバーをGoで実装するための基盤を提供していました。
技術的詳細
このコミットの技術的詳細は、exp/sshパッケージ内のmessages.goファイルにあるparseNameList関数の挙動の変更に集約されます。
parseNameList関数は、SSHプロトコルで用いられる「名前リスト」(NameList)という形式のデータを解析し、Goの[]stringスライスに変換する役割を担っています。名前リストは、通常、カンマで区切られた文字列のリストとして表現されます。
元の実装では、parseNameList関数が空の入力(contentsの長さが0)を受け取った場合、特に何もせずにreturnしていました。Goの関数の戻り値は、明示的に設定されない場合、その型のゼロ値が返されます。[]stringのゼロ値はnilスライスです。したがって、空の入力に対してはnilスライスが返されていました。
このコミットでは、この挙動を変更し、空の入力に対しては明示的に長さゼロの[]string(空のスライス)を返すように修正しています。これは、emptyNameList = []string{}というグローバル変数を導入し、空の入力の場合にout = emptyNameListと代入することで実現されています。
この変更の理由は、テストの観点からの一貫性です。たとえfor rangeループでnilスライスと空のスライスが同じように扱われるとしても、テストでは特定の期待値(この場合は空のスライス)が求められることがあります。APIの利用者にとっても、空の入力に対して常に空のスライスが返される方が、nilが返されるよりも予測可能で扱いやすい場合があります。
また、pkg/Makefileへのexp/sshの追加は、Goのビルドシステムがこの実験的なパッケージを認識し、適切にビルド対象に含めるための設定変更です。これにより、exp/sshパッケージがGoの標準ライブラリの一部として、他のパッケージと同様に扱われるようになります。
コアとなるコードの変更箇所
変更はsrc/pkg/exp/ssh/messages.goファイルにあります。
--- a/src/pkg/exp/ssh/messages.go
+++ b/src/pkg/exp/ssh/messages.go
@@ -392,7 +392,10 @@ func parseString(in []byte) (out, rest []byte, ok bool) {
return
}
-var comma = []byte{','}
+var (
+ comma = []byte{','}
+ emptyNameList = []string{}
+)
func parseNameList(in []byte) (out []string, rest []byte, ok bool) {
contents, rest, ok := parseString(in)
@@ -400,6 +403,7 @@ func parseNameList(in []byte) (out []string, rest [][]byte, ok bool) {
return
}
if len(contents) == 0 {
+ out = emptyNameList
return
}
parts := bytes.Split(contents, comma)
コアとなるコードの解説
変更点1: グローバル変数の追加
-var comma = []byte{','}
+var (
+ comma = []byte{','}
+ emptyNameList = []string{}
+)
この部分では、既存のcomma変数の定義に加えて、新しくemptyNameListというグローバル変数が追加されています。
comma = []byte{','}: カンマ文字のバイトスライスを定義しています。これはbytes.Split関数で区切り文字として使用されます。emptyNameList = []string{}: 長さゼロの空の[]stringスライスを定義しています。この変数は、parseNameList関数が空の名前リストを処理する際に、nilスライスの代わりに返されるようになります。グローバル変数として定義することで、毎回新しい空のスライスを作成するオーバーヘッドを避けることができます。
変更点2: parseNameList関数の修正
func parseNameList(in []byte) (out []string, rest []byte, ok bool) {
contents, rest, ok := parseString(in)
if !ok {
return
}
if len(contents) == 0 {
+ out = emptyNameList
return
}
parts := bytes.Split(contents, comma)
この部分が、空のNameListの処理ロジックの核心です。
contents, rest, ok := parseString(in): 入力バイトスライスinから文字列部分を解析し、contentsに格納します。if !ok { return }:parseStringが失敗した場合、そのまま関数を終了します。if len(contents) == 0 { ... }: ここが今回の修正のポイントです。もし解析されたcontentsが空(長さが0)である場合、つまり空の名前リストが与えられた場合、以下の処理が行われます。out = emptyNameList: 戻り値のスライスoutに、先ほど定義したグローバルな空のスライスemptyNameListを代入します。これにより、空の入力に対してnilスライスではなく、長さゼロの空のスライスが返されることが保証されます。return: 関数を終了します。
この変更により、parseNameList関数は、空の入力に対して常に一貫して長さゼロの空のスライスを返すようになり、テストの期待値との不一致が解消されます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語の
bytesパッケージ: https://pkg.go.dev/bytes
参考にした情報源リンク
- Go slices nil vs empty:
- Go bytes.Split:
- Go
exp/ssh(直接的な情報は見つかりませんでしたが、Goの実験的パッケージの一般的な情報として):- Goの実験的パッケージに関する一般的な情報源(例: Goのリリースノートやメーリングリストのアーカイブなど)