[インデックス 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のリリースノートやメーリングリストのアーカイブなど)