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

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

このコミットは、Go言語のencoding/binaryパッケージにおいて、構造体のエンコードおよびデコード時に、ブランク識別子_で命名されたフィールド(いわゆる「ブランクフィールド」)をスキップする機能を追加するものです。これにより、固定サイズのバイナリフォーマットや、C言語の構造体との相互運用性において、パディング目的で挿入されたフィールドを適切に処理できるようになります。

コミット

commit 27c990e7946d69fecc0c823e54f7d7da631ed1a5
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Nov 1 12:39:20 2012 -0700

    encoding/binary: skip blank fields when (en/de)coding structs
    
    - minor unrelated cleanups
    - performance impact in the noise
    
    benchmark                       old ns/op    new ns/op    delta
    BenchmarkReadSlice1000Int32s        83462        83346   -0.14%
    BenchmarkReadStruct                  4141         4247   +2.56%
    BenchmarkReadInts                    1588         1586   -0.13%
    BenchmarkWriteInts                   1550         1489   -3.94%
    BenchmarkPutUvarint32                  39           39   +1.02%
    BenchmarkPutUvarint64                 142          144   +1.41%
    
    benchmark                        old MB/s     new MB/s  speedup
    BenchmarkReadSlice1000Int32s        47.93        47.99    1.00x
    BenchmarkReadStruct                 16.90        16.48    0.98x
    BenchmarkReadInts                   18.89        18.91    1.00x
    BenchmarkWriteInts                  19.35        20.15    1.04x
    BenchmarkPutUvarint32              101.90       100.82    0.99x
    BenchmarkPutUvarint64               56.11        55.45    0.99x
    
    Fixes #4185.
    
    R=r, rsc
    CC=golang-dev
    https://golang.org/cl/6750053

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

https://github.com/golang/go/commit/27c990e7946d69fecc0c823e54f7d7da631ed1a5

元コミット内容

encoding/binary: skip blank fields when (en/de)coding structs

- minor unrelated cleanups
- performance impact in the noise

benchmark                       old ns/op    new ns/op    delta
BenchmarkReadSlice1000Int32s        83462        83346   -0.14%
BenchmarkReadStruct                  4141         4247   +2.56%
BenchmarkReadInts                    1588         1586   -0.13%
BenchmarkWriteInts                   1550         1489   -3.94%
BenchmarkPutUvarint32                  39           39   +1.02%
BenchmarkPutUvarint64                 142          144   +1.41%

benchmark                        old MB/s     new MB/s  speedup
BenchmarkReadSlice1000Int32s        47.93        47.99    1.00x
BenchmarkReadStruct                 16.90        16.48    0.98x
BenchmarkReadInts                   18.89        18.91    1.00x
BenchmarkWriteInts                  19.35        20.15    1.04x
BenchmarkPutUvarint32              101.90       100.82    0.99x
BenchmarkPutUvarint64               56.11        55.45    0.99x

Fixes #4185.

R=r, rsc
CC=golang-dev
https://golang.org/cl/6750053

変更の背景

この変更は、Goのencoding/binaryパッケージが、構造体内の特定のフィールドを適切に処理できないという問題(Issue #4185)を解決するために行われました。具体的には、Goの構造体において、ブランク識別子_(アンダースコア)で命名されたフィールドや、エクスポートされていない(小文字で始まる)フィールドが存在する場合、encoding/binaryパッケージがそれらをスキップせず、エンコード/デコード処理が失敗するか、意図しない動作を引き起こす可能性がありました。

このようなブランクフィールドは、特に以下のようなシナリオで重要になります。

  1. パディング: C言語などで定義された固定サイズのバイナリ構造体とGoの構造体を相互運用する場合、アライメントのために明示的なパディングバイトが必要になることがあります。Goでは、このようなパディングを_フィールドとして表現することが一般的です。
  2. 予約済みフィールド: 将来の拡張のために予約された領域や、特定のプロトコルで定義された未使用のバイト列を表現する場合にも、_フィールドが利用されます。

このコミット以前は、encoding/binaryはこれらのフィールドを通常のフィールドと同様に扱おうとし、結果としてエラーになったり、バイナリデータの読み書きが期待通りに行われないという問題がありました。この変更により、encoding/binaryはブランクフィールドを「スキップすべき」ものとして認識し、読み込み時にはその分のバイトを読み飛ばし、書き込み時にはゼロ値で埋めることで、これらのユースケースに対応できるようになりました。

前提知識の解説

encoding/binaryパッケージ

encoding/binaryパッケージは、Goの基本的なデータ型(整数、浮動小数点数、ブール値など)や、それらを含む構造体を、バイト列との間で変換(エンコード/デコード)するための機能を提供します。ネットワークプロトコル、ファイルフォーマット、または異なるシステム間でのデータ交換など、バイトオーダー(エンディアン)が重要なバイナリデータの扱いに特化しています。

  • binary.Read(r io.Reader, order ByteOrder, data interface{}) error: io.Readerからバイトを読み込み、指定されたバイトオーダーに従ってdata(ポインタである必要がある)にデコードします。
  • binary.Write(w io.Writer, order ByteOrder, data interface{}) error: dataの値を指定されたバイトオーダーに従ってバイト列にエンコードし、io.Writerに書き込みます。

Goのreflectパッケージ

encoding/binaryパッケージは、Goのreflectパッケージを内部で利用して、interface{}型のdata引数に渡された値の型情報を実行時に動的に検査し、その構造体のフィールドを列挙して処理します。reflectパッケージは、Goの型システムをプログラム的に操作するための強力なツールであり、以下のような機能を提供します。

  • reflect.ValueOf(i interface{}) reflect.Value: インターフェース値のreflect.Value表現を返します。
  • reflect.Value.Kind(): 値の具体的な種類(reflect.Struct, reflect.Int, reflect.Sliceなど)を返します。
  • reflect.Value.NumField(): 構造体のフィールド数を返します。
  • reflect.Value.Field(i int) reflect.Value: 構造体のi番目のフィールドのreflect.Valueを返します。
  • reflect.Value.CanSet(): そのreflect.Valueが変更可能(セット可能)であるかどうかを返します。これは、エクスポートされたフィールド(大文字で始まるフィールド)や、ポインタを介してアクセスされる値に対してtrueを返します。

Goにおけるブランク識別子_ (アンダースコア)

Go言語では、ブランク識別子_は、変数を宣言したがその値を使用しない場合や、関数の戻り値の一部を無視する場合など、意図的に値を破棄することを示すために使用されます。構造体のフィールド名として_を使用することも可能で、これはそのフィールドがGoのプログラムロジックからは直接アクセスされないことを示します。

例えば、以下のような構造体は、_フィールドをパディングとして使用する典型的な例です。

type Header struct {
    Magic   uint32
    Version uint16
    _       uint16 // Padding to align next field on a 4-byte boundary
    Length  uint32
}

この_ uint16フィールドは、MagicVersionの後に2バイトのパディングを挿入し、Lengthフィールドが4バイト境界に配置されるようにするために使われます。

技術的詳細

このコミットの主要な技術的変更点は、encoding/binaryパッケージがreflectを使用して構造体を処理する際に、ブランクフィールドを特別扱いするロジックを追加したことです。

  1. ReadおよびWrite関数のコメント更新: Read関数とWrite関数のドキュメントに、ブランクフィールドの扱いに関する説明が追加されました。

    • Readの場合:「構造体への読み込み時、ブランク(_)フィールド名のフィールドデータはスキップされます。つまり、ブランクフィールド名はパディングに使用できます。」
    • Writeの場合:「構造体の書き込み時、ブランク(_)フィールド名のフィールドにはゼロ値が書き込まれます。」
  2. decoderencoderのリファクタリング: 以前はdecoderencoderがそれぞれ独立した構造体でしたが、共通の基盤となるcoder構造体が導入され、type decoder coderおよびtype encoder coderとして定義されるようになりました。これはコードの重複を減らし、保守性を向上させるためのマイナーなクリーンアップです。

  3. ブランクフィールドの検出とスキップロジック: decoder.valueおよびencoder.valueメソッド内で、構造体のフィールドをイテレートする際に、各フィールドがブランクフィールドであるかどうかを判定するロジックが追加されました。

    // binary.go (decoder.value, encoder.value 共通のロジック)
    if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" {
        // 通常のフィールドとして処理
        d.value(v) // または e.value(v)
    } else {
        // ブランクフィールドとしてスキップ
        d.skip(v) // または e.skip(v)
    }
    
    • v.CanSet(): この条件は、フィールドがエクスポートされている(大文字で始まる)か、またはポインタを介してセット可能である場合にtrueを返します。encoding/binaryは通常、セット可能なフィールドのみを処理します。
    • t.Field(i).Name != "_": この条件は、フィールド名がブランク識別子_ではない場合にtrueを返します。
    • この||(OR)条件により、フィールドがCanSet()であるか、またはフィールド名が_ではない場合にのみ、通常のエンコード/デコード処理(d.value(v)またはe.value(v))が実行されます。
    • それ以外の場合(つまり、CanSet()falseで、かつフィールド名が_である場合)、新しく追加されたskipメソッドが呼び出されます。この最適化は、StructField情報を毎回生成するコストを避けるために行われています。
  4. skipメソッドの実装:

    • decoder.skip(v reflect.Value): デコーダの場合、スキップするフィールドのサイズ分だけ内部バッファ(d.buf)のポインタを進めます。これにより、そのフィールドのバイトデータは読み飛ばされます。
      func (d *decoder) skip(v reflect.Value) {
          d.buf = d.buf[dataSize(v):]
      }
      
    • encoder.skip(v reflect.Value): エンコーダの場合、スキップするフィールドのサイズ分だけ内部バッファ(e.buf)をゼロで埋め、その後バッファのポインタを進めます。これにより、ブランクフィールドに対応するバイト列がゼロで埋められます。
      func (e *encoder) skip(v reflect.Value) {
          n := dataSize(v)
          for i := range e.buf[0:n] {
              e.buf[i] = 0
          }
          e.buf = e.buf[n:]
      }
      
  5. テストケースの追加: binary_test.goTestBlankFieldsという新しいテスト関数が追加されました。このテストは、BlankFieldsという構造体(ブランクフィールドを含む)を定義し、それが正しくエンコード/デコードされることを検証します。特に、書き込み時にブランクフィールドがゼロで埋められ、読み込み時にそれらがスキップされることを確認しています。

ベンチマーク結果は、この変更がパフォーマンスに大きな影響を与えないことを示しており、ほとんどのケースで「ノイズの範囲内」とされています。

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

src/pkg/encoding/binary/binary.go

--- a/src/pkg/encoding/binary/binary.go
+++ b/src/pkg/encoding/binary/binary.go
@@ -125,6 +125,9 @@ func (bigEndian) GoString() string { return "binary.BigEndian" }
 // of fixed-size values.
 // Bytes read from r are decoded using the specified byte order
 // and written to successive fields of the data.
+// When reading into structs, the field data for fields with
+// blank (_) field names is skipped; i.e., blank field names
+// may be used for padding.
 func Read(r io.Reader, order ByteOrder, data interface{}) error {
 	// Fast path for basic types.
 	if n := intDestSize(data); n != 0 {
@@ -154,7 +157,7 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error {
 		return nil
 	}
 
-	// Fallback to reflect-based.
+	// Fallback to reflect-based decoding.
 	var v reflect.Value
 	switch d := reflect.ValueOf(data); d.Kind() {
 	case reflect.Ptr:
@@ -181,6 +184,8 @@ func Read(r io.Reader, order ByteOrder, data interface{}) error {
 // values, or a pointer to such data.
 // Bytes written to w are encoded using the specified byte order
 // and read from successive fields of the data.
+// When writing structs, zero values are are written for fields
+// with blank (_) field names.
 func Write(w io.Writer, order ByteOrder, data interface{}) error {
 	// Fast path for basic types.
 	var b [8]byte
@@ -239,6 +244,8 @@ func Write(w io.Writer, order ByteOrder, data interface{}) error {
 		_, err := w.Write(bs)
 		return err
 	}\n+\n+\t// Fallback to reflect-based encoding.\n \tv := reflect.Indirect(reflect.ValueOf(data))\n \tsize := dataSize(v)\n \tif size < 0 {\n@@ -300,15 +307,13 @@ func sizeof(t reflect.Type) int {\n 	return -1\n }\n \n-type decoder struct {\n+type coder struct {\n 	order ByteOrder\n 	buf   []byte\n }\n \n-type encoder struct {\n-\torder ByteOrder\n-\tbuf   []byte\n-}\n+type decoder coder\n+type encoder coder\n \n func (d *decoder) uint8() uint8 {\n \tx := d.buf[0]\
@@ -379,9 +384,19 @@ func (d *decoder) value(v reflect.Value) {\n \t\t}\n \n \tcase reflect.Struct:\n+\t\tt := v.Type()\n \t\tl := v.NumField()\n \t\tfor i := 0; i < l; i++ {\n-\t\t\td.value(v.Field(i))\n+\t\t\t// Note: Calling v.CanSet() below is an optimization.\n+\t\t\t// It would be sufficient to check the field name,\n+\t\t\t// but creating the StructField info for each field is\n+\t\t\t// costly (run \"go test -bench=ReadStruct\" and compare\n+\t\t\t// results when making changes to this code).\n+\t\t\tif v := v.Field(i); v.CanSet() || t.Field(i).Name != \"_\" {\n+\t\t\t\td.value(v)\n+\t\t\t} else {\n+\t\t\t\td.skip(v)\n+\t\t\t}\n \t\t}\n \n \tcase reflect.Slice:\
@@ -435,9 +450,15 @@ func (e *encoder) value(v reflect.Value) {\n \t\t}\n \n \tcase reflect.Struct:\n+\t\tt := v.Type()\n \t\tl := e.NumField()\n \t\tfor i := 0; i < l; i++ {\n-\t\t\te.value(v.Field(i))\n+\t\t\t// see comment for corresponding code in decoder.value()\n+\t\t\tif v := v.Field(i); v.CanSet() || t.Field(i).Name != \"_\" {\n+\t\t\t\te.value(v)\n+\t\t\t} else {\n+\t\t\t\te.skip(v)\n+\t\t\t}\n \t\t}\n \n \tcase reflect.Slice:\
@@ -492,6 +513,18 @@ func (e *encoder) value(v reflect.Value) {\n \t}\n }\n \n+func (d *decoder) skip(v reflect.Value) {\n+\td.buf = d.buf[dataSize(v):]\n+}\n+\n+func (e *encoder) skip(v reflect.Value) {\n+\tn := dataSize(v)\n+\tfor i := range e.buf[0:n] {\n+\t\te.buf[i] = 0\n+\t}\n+\te.buf = e.buf[n:]\n+}\n+\n // intDestSize returns the size of the integer that ptrType points to,\n // or 0 if the type is not supported.\

src/pkg/encoding/binary/binary_test.go

--- a/src/pkg/encoding/binary/binary_test.go
+++ b/src/pkg/encoding/binary/binary_test.go
@@ -120,18 +120,14 @@ func testWrite(t *testing.T, order ByteOrder, b []byte, s1 interface{}) {
 	checkResult(t, "Write", order, err, buf.Bytes(), b)
 }\n \n-func TestBigEndianRead(t *testing.T) { testRead(t, BigEndian, big, s) }\n-\n-func TestLittleEndianRead(t *testing.T) { testRead(t, LittleEndian, little, s) }\n-\n-func TestBigEndianWrite(t *testing.T) { testWrite(t, BigEndian, big, s) }\n-\n-func TestLittleEndianWrite(t *testing.T) { testWrite(t, LittleEndian, little, s) }\n+func TestLittleEndianRead(t *testing.T)     { testRead(t, LittleEndian, little, s) }\n+func TestLittleEndianWrite(t *testing.T)    { testWrite(t, LittleEndian, little, s) }\n+func TestLittleEndianPtrWrite(t *testing.T) { testWrite(t, LittleEndian, little, &s) }\n \n+func TestBigEndianRead(t *testing.T)     { testRead(t, BigEndian, big, s) }\n+func TestBigEndianWrite(t *testing.T)    { testWrite(t, BigEndian, big, s) }\n func TestBigEndianPtrWrite(t *testing.T) { testWrite(t, BigEndian, big, &s) }\n \n-func TestLittleEndianPtrWrite(t *testing.T) { testWrite(t, LittleEndian, little, &s) }\n-\n func TestReadSlice(t *testing.T) {\n 	slice := make([]int32, 2)\n 	err := Read(bytes.NewBuffer(src), BigEndian, slice)\
@@ -147,20 +143,75 @@ func TestWriteSlice(t *testing.T) {\n func TestWriteT(t *testing.T) {\n 	buf := new(bytes.Buffer)\n 	ts := T{}\n-\terr := Write(buf, BigEndian, ts)\n-\tif err == nil {\n-\t\tt.Errorf(\"WriteT: have nil, want non-nil\")\n+\tif err := Write(buf, BigEndian, ts); err == nil {\n+\t\tt.Errorf(\"WriteT: have err == nil, want non-nil\")\n \t}\n \n \ttv := reflect.Indirect(reflect.ValueOf(ts))\n \tfor i, n := 0, tv.NumField(); i < n; i++ {\n-\t\terr = Write(buf, BigEndian, tv.Field(i).Interface())\n-\t\tif err == nil {\n-\t\t\tt.Errorf(\"WriteT.%v: have nil, want non-nil\", tv.Field(i).Type())\n+\t\tif err := Write(buf, BigEndian, tv.Field(i).Interface()); err == nil {\n+\t\t\tt.Errorf(\"WriteT.%v: have err == nil, want non-nil\", tv.Field(i).Type())\n \t\t}\n \t}\n }\n \n+type BlankFields struct {\n+\tA uint32\n+\t_ int32\n+\tB float64\n+\t_ [4]int16\n+\tC byte\n+\t_ [7]byte\n+\t_ struct {\n+\t\tf [8]float32\n+\t}\n+}\n+\n+type BlankFieldsProbe struct {\n+\tA  uint32\n+\tP0 int32\n+\tB  float64\n+\tP1 [4]int16\n+\tC  byte\n+\tP2 [7]byte\n+\tP3 struct {\n+\t\tF [8]float32\n+\t}\n+}\n+\n+func TestBlankFields(t *testing.T) {\n+\tbuf := new(bytes.Buffer)\n+\tb1 := BlankFields{A: 1234567890, B: 2.718281828, C: 42}\n+\tif err := Write(buf, LittleEndian, &b1); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\n+\t// zero values must have been written for blank fields\n+\tvar p BlankFieldsProbe\n+\tif err := Read(buf, LittleEndian, &p); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\n+\t// quick test: only check first value of slices\n+\tif p.P0 != 0 || p.P1[0] != 0 || p.P2[0] != 0 || p.P3.F[0] != 0 {\n+\t\tt.Errorf(\"non-zero values for originally blank fields: %#v\", p)\n+\t}\n+\n+\t// write p and see if we can probe only some fields\n+\tif err := Write(buf, LittleEndian, &p); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\n+\t// read should ignore blank fields in b2\n+\tvar b2 BlankFields\n+\tif err := Read(buf, LittleEndian, &b2); err != nil {\n+\t\tt.Error(err)\n+\t}\n+\tif b1.A != b2.A || b1.B != b2.B || b1.C != b2.C {\n+\t\tt.Errorf(\"%#v != %#v\", b1, b2)\n+\t}\n+}\n+\n type byteSliceReader struct {\n 	remain []byte\n }\

コアとなるコードの解説

binary.goの変更点

  1. ReadおよびWrite関数のドキュメント更新: これらの関数のコメントに、ブランクフィールドの扱いに関する新しい動作が明記されました。これは、ユーザーがencoding/binaryパッケージを使用する際の重要な情報となります。

  2. coder型の導入とdecoder/encoderのリファクタリング: 以前はdecoderencoderがそれぞれorder ByteOrderbuf []byteを持つ独立した構造体でしたが、これらを共通のcoder構造体としてまとめ、type decoder codertype encoder coderとすることで、コードの簡潔性と再利用性が向上しました。機能的な変更というよりは、内部的な構造の改善です。

  3. decoder.valueおよびencoder.valueにおけるブランクフィールド処理: これがこのコミットの最も重要な変更点です。構造体のフィールドを処理するループ内で、以下の条件が追加されました。

    if v := v.Field(i); v.CanSet() || t.Field(i).Name != "_" {
        // ... 通常のエンコード/デコード処理 ...
    } else {
        // ... ブランクフィールドのスキップ処理 ...
    }
    
    • v.Field(i): 現在処理している構造体フィールドのreflect.Valueを取得します。
    • v.CanSet(): このフィールドがGoのプログラムから値を設定できる(つまり、エクスポートされている)かどうかをチェックします。
    • t.Field(i).Name != "_": このフィールドの名前がブランク識別子_ではないかをチェックします。
    • この条件式は、「もしフィールドが設定可能であるか、またはフィールド名が_ではないならば」という論理を表します。
      • フィールドが設定可能(エクスポートされている)であれば、それは通常のフィールドとして扱われます。
      • フィールドが設定不可能(エクスポートされていない)であっても、その名前が_でなければ、それは通常のフィールドとして扱われます(ただし、CanSet()falseのため、encoding/binaryは通常そのフィールドをスキップします。このコミットは_フィールドの明示的なスキップを追加しています)。
      • 重要なのは、CanSet()falseであり、かつフィールド名が_である場合です。 この場合にのみ、elseブロックが実行され、skipメソッドが呼び出されます。これにより、encoding/binaryは明示的にブランクフィールドを無視するようになります。
  4. skipメソッドの追加:

    • func (d *decoder) skip(v reflect.Value): decoderの場合、dataSize(v)(フィールドのバイトサイズ)分だけ内部バッファd.bufの先頭を切り詰めます。これにより、対応するバイトデータが読み飛ばされ、次のフィールドの処理に進みます。
    • func (e *encoder) skip(v reflect.Value): encoderの場合、dataSize(v)分だけ内部バッファe.bufの該当部分をゼロで埋めます。その後、バッファの先頭を切り詰めます。これにより、ブランクフィールドに対応するバイト列がゼロで埋められ、バイナリ出力の整合性が保たれます。

binary_test.goの変更点

  1. 既存テストの並び替え: 既存のテスト関数(TestBigEndianRead, TestLittleEndianReadなど)の宣言順序が変更されました。これは機能的な変更ではありません。

  2. TestWriteTの修正: エラーチェックのロジックがよりGoらしい書き方(if err := ...; err == nil)に修正されました。

  3. BlankFieldsおよびBlankFieldsProbe構造体の追加:

    • BlankFields: uint32, float64, byteといった通常のフィールドと、int32, [4]int16, [7]byte, 匿名構造体(その中に[8]float32)といった様々な型のブランクフィールド_を含む構造体です。
    • BlankFieldsProbe: BlankFieldsと全く同じメモリレイアウトを持つが、すべてのフィールドに名前が付いている構造体です。これは、BlankFieldsをエンコード/デコードした際に、ブランクフィールドに対応するメモリ領域に何が書き込まれたか(特にゼロ値が書き込まれたか)を検証するために使用されます。
  4. TestBlankFields関数の追加: この新しいテストは、ブランクフィールドのエンコード/デコードが正しく機能することを包括的に検証します。

    • BlankFieldsのインスタンスb1を作成し、Writeでバイト列にエンコードします。
    • そのバイト列をBlankFieldsProbeのインスタンスpReadでデコードします。ここで、pのブランクフィールドに対応する部分(P0, P1, P2, P3.F)がすべてゼロ値であることをアサートします。これは、Writeがブランクフィールドをゼロで埋めたことを確認します。
    • 次に、pWriteでエンコードします。
    • そのバイト列を再びBlankFieldsのインスタンスb2Readでデコードします。ここで、b2の通常のフィールド(A, B, C)が元のb1の値と一致することをアサートします。これは、Readがブランクフィールドを正しくスキップし、通常のフィールドを正しくデコードしたことを確認します。

これらの変更により、encoding/binaryパッケージは、Goの構造体におけるブランクフィールドを、パディングや予約済み領域として適切に扱えるようになり、より柔軟なバイナリデータ処理が可能になりました。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント: encoding/binaryパッケージ
  • Go言語公式ドキュメント: reflectパッケージ
  • Go言語におけるブランク識別子(_)に関する一般的な情報源
    • (例: Go言語のチュートリアルや公式ブログ記事など)