Open10

memo:初めてのGo言語

よつよつ

1. Go 環境のセットアップ

インストール

(今後変更される可能性があるため、公式 docs を参照することをオススメします)
https://go.dev/doc/install

  • mac (Homebrew)
brew install go
  • windows(Chocolatey)
choco install golang
  • Linux/FreeBSD
    • 本書ではtarファイルをダウンロードして解答する方法が推奨されている
    • 私は asdf を用いて管理しているので、代わりにこれを書いておきます(参照
asdf plugin add golang https://github.com/asdf-community/asdf-golang.git
asdf install golang latest

バージョン確認

go version
よつよつ

go コマンド

go run

.go ファイルを実行する。
後述する go build を使用する実行方法と異なり、ビルド結果を一時フォルダに格納し、実行後は削除される。

go run hello.go

go build

実行形式のバイナリファイルを作成する。
たとえば hello.go というファイルをビルドしたとき、hello (Windows では hello.exe)というファイルが生成される。

go build hello.go

-o オプションを指定することで、生成されるバイナリファイルに名前を付けることもできる。

go build -o hello_world hello.go

生成されたバイナリファイルはそのまま実行できる。

./hello_world

go mod

Go モジュール(go.mod ファイルが設置されたディレクトリ)を扱うための各種コマンドを提供する。

go mod init

ディレクトリに go.mod ファイルを作成し、ディレクトリを Go モジュールとして初期化する。
引数にはモジュール名を指定する。

go mod init hello-world

モジュールを公開する予定がある場合は、公開するリポジトリの場所と機能を表す名称を指定することが推奨されている。

go mod init github.com/hello-world

コマンドを実行すると、以下のように go.mod ファイルが生成される。

module hello-world

go 1.19

go mod tidy

モジュールのソースファイルを解析し、必要なサードパーティのライブラリをダウンロードしたり、不要になったファイルを削除したりする。

go mod tidy

go install

公開されている Go パッケージをインストールする。

go install hello-world

バージョン指定が必要な場合は、モジュール名の最後に @ をつけて指定する。
@latest を指定すると、最新のモジュールがインストールされる。すでに同様のツールがインストールされている場合は更新される。

go install hello-world@latest

go fmt

コードを、公式が推奨する標準形式にフォーマットする。

go fmt

goimports

外部ツール。go fmt の機能を強化したもの。主に import 文のクリーンアップを行う。

https://pkg.go.dev/golang.org/x/tools/cmd/goimports

-l フラグはフォーマットに沿っていないファイル名を出力、-w フラグはツールによる直接の修正を許可します。. にはチェック対象のディレクトリパスを記載します。

go install golang.org/x/tools/cmd/goimports@latest
goimports -l -w .

go lint

構文的には問題がないが、期待通りに動いてくれないコードを未然に検査する。
たとえばメソッドに渡す変数の数を間違えていたり、使われていない変数に値を代入したりしている、など。

go vet

staticcheck

外部ツール。
いわゆる linterであり、コードスタイルを統一する。

https://staticcheck.dev/

go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck

golangci-lint

外部ツール。
linter や go vet などのコード検査ツールを同時に実行する。

https://github.com/golangci/golangci-lint

curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.3.0
golangci-lint run
よつよつ

2. 基本型と宣言

ゼロ値

変数を宣言するのみで代入しない場合、デフォルトで各型に設定された ゼロ値 が挿入される。

var n int        // 0
var s string    // ""

リテラル

4種類存在する。

整数

数字の並び。
0b 0o 0x をつけることで、それぞれ「2進数」「8進数」「16進数」を表現できる。

var d int = 100       // 10進数 (demical)
var b int = 0b100   // 2進数 (binary)
var o int = 0o100   // 8進数 (octal)
var x int = 0x100   // 16進数 (hexademical)

また、_ が使用できる。これは値に勘案されず、単に仕切り文字として機能する。

var price = 123_456_789   // 123456789

浮動小数点

IEEE754 に準拠した少数。
一般的な表記のほか、指数を使った表現も可能。

var f1 float64 = 1.234
var f2 float64 = 6.04e23

rune

多言語の char に相当。シングルクォーテーション(')で囲って表現する。
1文字の Unicode 文字を示す。

var r rune = '\u0061'   // a

文字列

以下の2つの種類が存在する。
後者は \ " のような特殊な文字を使用したり、複数行記述するときに扱う。

  • ダブルクォーテーション(")を用いる(解釈済み文字列リテラル)
  • バッククォート(`)を用いる(生の文字リテラル)
var s1 string = "hoge"
var s2 string = `"huga" "piyo"`

リテラルと型

Go において、リテラルは 肩を持たない(untyped)
変数にリテラルが代入されるとき、リテラルそれぞれがもつ型への互換性チェックが行われる。これにより、(整数値が文字列型に代入されるなどの)不正な代入が防がれる。

後述するような型を指定しない代入の場合、リテラルそれぞれに定められた デフォルトの型 が適用される。

論理型

true false のみを格納する。

var flag bool = true

数値型

いくつかにわけて紹介する。

整数型

符号あり(int)符号なし(uint) それぞれに、バイト数を示す整数をつけて4種類存在する。
たとえば uint32 は「符号なし 32 bit 整数」のような感じ。

特別な整数型

  • byteuint8 の別名。
  • int:CPU によって 32 bit もしくは 64 bit の符号付き整数になる。int32 int64 とは別であり、演算を行うとコンパイルエラーになる点に注意。
  • uintint の符号なし版。
  • rune:先述した「rune」と同一。

計算演算子

+ - * / % が使用できる。

  • 整数の割り算は 0 の方向に丸められる。結果が浮動小数点数でほしい場合は、計算前に型変換を行う必要がある。
  • 計算結果を再代入するための演算子が存在する(+=, -=, *=, /=, %=)。

比較演算子

== != < > <= >= が使用できる。

ビット演算子

<< >> でシフト、& | ^ &^ で各演算ができる。
計算演算子と同様、計算結果の再代入のための演算子もある。

浮動小数点数

float32float64 がある。基本は後者を用いる。

  • 非ゼロの浮動小数点数を 0 で割ると、 +Inf または -Inf になる。
  • ゼロの浮動小数点数を 0 で割ると、NaN になる。
  • 比較演算に == != を用いるのは推奨されない。代わりに、ふたつの値の差が許容誤差以内かをチェックするとよい。

文字列型 / rune 型

ゼロ値は空文字列("")である。

数値型と同様、比較演算に == !== などが使用できる。
< > <= >= を使用した場合は、辞書順にのっとって比較される。
また、+ による結合もサポートしている。

明示的型変換

Go は明快さと可読性に重きを置いているため、変数間で暗黙的に型変換が行われることはない
例えば、以下の式はエラーになる。

var x int = 100
var y float64 = 1.23

var bad float64 = x * y   // エラー。両変数の型が違う
var good float64 = float64(x) * y   // これなら OK

同様の理由により、論理型との比較も行えない。いわゆる「truthy」「falsy」な値の概念は存在しない。
論理型に変換する場合は、比較演算子を使用する必要がある。

var s string
if s {   // エラー。s は論理型ではない
    fmt.Println("bad")
} 
if s == "" {
    fmt.Println("good")
}

変数宣言

  • var を使用する
  • := を使用する
    の二種類。

var を使用する

パッケージレベルで宣言する変数に多く用いられる。理由は後述。
また、ゼロ値で初期化したり、明示的に型を指定したい場合もこちらを利用する。

// 基本形
var x int = 10

// 型は省略することもできる
// この場合、リテラルそれぞれに割り当てられたデフォルト型が適用される
var x = 10   // int

// 初期化も省略できる
// この場合も、リテラルそれぞれのゼロ値が適用される
var x int   // x=0

いわゆるシュガーシンタックス(便利な記法)もいくつかある。

// 複数の変数をまとめて初期化できる
var x, y int = 10, 20

// 別の型の変数をまとめて初期化できる
var s, n = "hoge", 10

// 宣言リストにまとめると、複数の変数をまとめて初期化できる
var (
    s            = "hoge"
    n       int
    f1, f2      = true, false 
)

:= を使用する

関数内でのみ使用できる、変数の宣言と初期化を一手に行える演算子。
この制約のため、パッケージ単位での変数定義には使用できない。

また、後述する再代入の挙動にも注意。

// 基本形
x := 10

// 複数の変数をまとめて初期化できる
x, y := 10, "hello"

// すでに存在する変数に対して使用した場合、再代入になるので注意
x := 10
x := 20
fmt.Println(x)   // 20

定数

Go の定数はいわゆる「変更不可能な変数」ではなく、「名前を持ったリテラル」という扱いになっている。そのため、代入可能なものは基本的なリテラル(と一部の例外)に限られる。
多言語のように、変数がイミューダブル(不変)であることを宣言する方法がないことに注意。

// 基本形
const x int = 10

// 宣言リストが使用できる
const (
    x = 10
    y = "hello"
)

// 再代入しようとするとエラーになる
const x = 10
x = 20           // error: cannot assign to x

「名前付きのリテラル」と表現したが、代入操作の挙動には注意したい。
明示的に型を宣言した定数は、それ以外の型に自動変換されることはない。たとえば int で定義した定数を int64 の変数に代入する、といったことはできない。

const x int = 10
var y int64 = x   // cannot use x (constant 10 of type int) as int64 value in variable declaration

未使用変数

Go では、宣言されたが使用されていない変数はコンパイル時にエラーとなる。
これは Go の「大規模なチームによる共同開発を容易にする」という信念のもと設定されたルールである。

ちなみに再代入は許してくれる。

x := 10
fmt.Println(x)
x = 20

命名

変数

変数の命名は、

  • 文字、または_(アンダースコア)で始まる
  • 文字、数字、または_(アンダースコア)で構成される
    というルールがある。
    また、ロウワーキャメルケースが用いられることがほとんど(ex. hogeHuga)。

文字であればよいのでキリル文字や漢字も許容されるが、現実的ではない。

const 数値 int = 100
fmt.Println(数値)

余談だが、Go では一文字の変数名(ex. n)が使われることが多い。これはスコープの狭さを表現しており、イディオマチック(慣例的)な表現である。これを利用して、「長い変数名が多い=息の長い変数が多く、コードが複雑である」というコードの複雑性を示す指標とすることもできる。

定数

定数は変数のルールに加え、パッケージレベルの宣言の際に「パッケージ外からアクセスできるか」を「変数の頭が大文字かどうか」で決めている。
たとえば PublicValue なら外部からアクセスできるし、PrivateValue ならアクセスできない。

基底型

すべての型には 基底型 がある。

  • 型 T が基本型(論理型、数値型、文字列型)あるいは型リテラル(ex. 配列)の場合は、T 自身が基底型になる
  • それ以外の場合、T の宣言で参照している型が T の基底型になる
よつよつ

3. 合成型

「複合データ型」「コンテナ型」ともよばれる、複数のデータを格納するための型。

配列

(後述の理由で)一般には使用されない。
データの数が事前に予見でき、かつそれ以上増えないことを保証したい場合のみ使用する。

(これも後述の)比較できる構造体を含め、数少ない比較できる合成型。
ただし、サイズ数が同一の配列に限る。これは、配列が型とサイズ数まで含めた一つの型として扱われるため。たとえば [2]int[3]int は別物であるし、[2]int[2]string も別物であるため比較はできない。

// 基本形
var x [3]int
var x = [3]int{ 10, 20, 30 }

// 部分的に初期化したい場合は、インデックスを指定する
var x = [10]int{ 1:2, 3, 5:6 }   // [0, 2, 3, 0, 0, 6, 0, 0, 0]

// 初期化する場合、... を使用してサイズは省略できる
var x = [...]int{ 10, 20, 30 }

// 多次元配列(っぽいこと)もできる
var x [2][3]int

// 長さは len() で取得できる
var x [...]int{ 10, 20, 30 }
fmt.Println(len(x))   // 3

スライス

可変長の配列。
初期値は nil であり、「値がないこと」を示す識別子として機能する。スライスは nil 以外と比較ができない。

// 基本形
var x []int   // 初期値は nil
var x = []int{ 10, 20, 30 }

// 部分的に初期化したい場合は、インデックスを指定する
var x = []int{ 1:2, 3, 5:6 }   // [0, 2, 3, 0, 0, 6]

// 多次元配列(っぽいこと)もできる
var x [][]int

値を増やすには append を用いる。
go は 値渡しの言語 であるため、結果は帰り値として返される。そのため、再代入する必要がある。

var x []int   // []
x = append(x, 10)   // [10]

// 複数代入も可能
x = append(x, 20, 30, 40)   // [10, 20, 30, 40]

// ... でスライスを展開し、複数代入(統合)することもできる
var y = []int{ 60, 70, 80 }
x = append(x, y...)   // [ 10, 20, 30, 40, 50, 60, 70, 80 ]

スライスとキャパシティ

スライスでは、メモリ効率をよくするため キャパシティ というものが設定できる。
スライスはデータをメモリ内の連続した領域に格納することですばやいアクセスを可能にしている。そして、キャパシティを超える要素が追加されるとき、スライスの要素を新しいスペースにコピーし、キャパシティを増やす。
スライスの現在のキャパシティは cap で確認できる。

var x []int
fmt.Println(len(x), cap(x))   // 0 0

x = append(x, 10)
fmt.Println(len(x), cap(x))   // 1 1

x = append(x, 20, 30)
fmt.Println(len(x), cap(x))   // 3, 4

x = append(x, 40, 50)
fmt.Println(len(x), cap(x))   // 5, 8

例から推察できる通り、キャパシティは2倍ずつ増えていく(1,024 以上の場合は 25% ずつ)。
効率の良いプログラムを書くためには、このコピーをなるべく少なくするように書くことが求められる。そのためには、以下の make 関数が有用である。

make

スライスの作成方法として、make が存在する。
これは、あらかじめキャパシティを指定した状態でスライスを初期化する。

// 基本形
// 第二引数のみを指定した場合、同数のゼロ値が入る
x := make([]int, 8)
fmt.Println(len(x), cap(x))   // 8, 8

// 第三引数を指定することで、長さとキャパシティが異なるスライスを生成する
x := make([]int, 0, 10)
fmt.Println(len(x), cap(x))   // 0, 10

キャパシティを事前にあげておくことで、不要なコピーを減らすことができる。
要素の上限数は指定したくないが、要素数があらかた予見できる場合に使用するとよい方法。

スライスのスライス

Python と似た文法で、スライスを切り出したスライスを作成できる。1

x := []int{ 10, 20, 30, 40 }
fmt.Println(x[1:3])  // [20, 30]
fmt.Println(x[0:3]  // [10, 20, 30]
fmt.Println(x[1:]  // [20, 30, 40]

ここで注意しないといけないのが、スライス元とスライス先は メモリを共有している。つまり、スライス先に与えた変更は、スライス元にも反映される。

x := []int{ 10, 20, 30, 40 }
y := x[1:3]  // [20, 30]

y[0] = 10   // [10, 30]
fmt.Println(x)  // [10, 10, 30, 40]

また、スライス先に append したとき、スライス元にも追加が反映される。このとき、append 先にスライス元のデータがあった場合はそれを上書きする。

x := []int{ 10, 20, 30 }
y := x[:2]   // [ 10, 20 ]

y = append(y, 100)
fmt.Println(x)   // [ 10, 20, 100 ]

サブスライスへの append はこうした混乱を招くため、使用しないか、フルスライス式 を用いるとよい。また、copy を使用する方法もある(後述)。
スライス式に「利用できる親スライスのキャパシティの最後の位置」を追記することで、それ以降の append はメモリの別領域で処理するようになる。

x := []int{ 10, 20, 30 }
y := x[:2:2]   // キャパシティの終点を 2 に指定

y = append(y, 100)
fmt.Println(x)   // [ 10, 20, 30 ]

余談だが、配列からスライスを作成することもできる。

x := [3]int{ 10, 20, 30 }
y := x[0:2]   // [10, 20 ]

copy を使用する

copy は、第一引数のスライスに第二引数のスライスの要素をできる限りコピーする組み込み関数。先ほどのスライスとは違い、メモリを共有しない。
注意点として、こちらは与えられたスライスに直接変更を加え、返り値として変更した数を返すappend とはすこし挙動が違うので注意。

// 基本形
x := []int{10, 20, 30}
y := make([]int, 4)
num := copy(y, x)
fmt.Println(num, y) // 3 [ 10, 20, 30, 0 ]

// すでに値がある場合は、それを上書きする点に注意
x := []int{10, 20, 30}
y := []int{1, 2, 3, 4}
num := copy(y, x)
fmt.Println(num, y) // 3 [ 10, 20, 30, 4 ]

// 値の数より受け取る側の長さが小さい場合は、入る分だけコピーされる
x := []int{10, 20, 30}
y := make([]int, 2)
num := copy(y, x)
fmt.Println(num, y) // 2 [ 10, 20 ]

// サブスライスと組み合わせることで、部分的にコピーもできる
// これはメモリを共有しないため、完全な部分コピーとして機能する
x := []int{10, 20, 30}
y := make([]int, 4)
num := copy(y, x[1:])
fmt.Println(num, y) // 2, [ 20, 30, 0, 0 ]

文字列、rune、バイト

文字列もスライスと同様に部分的に切り出すことができる。
なお、文字列はイミューダブルなので(サブスライスのような)上書き問題は発生しない。が、あまり使うべきではない。

s := "Hello World!"
fmt.Println(s[:5])   // Hello

使うべきでない理由として、漢字や絵文字のような 1バイト文字ではない文字が含まれる可能性がある文字列に対してうまく機能しないことがある。これはスライスがあくまで「バイトごと」の値を切り出しているためであり、複数バイトの文字列の扱いには対応していないため。

s := "こんにちは、世界"
fmt.Println(s[:5])   // こ��

マップ

キーと値のペアによる合成型、マップを使用することができる。
注意点として、初期化時はマップリテラルの代入を行うこと。nil な map に対しての値の追加はパニックとなる。

// bad: nil な map に対して値の追加を行うとエラーになる
var nilMap map[string]string
nilMap["hoge"] = "foo"   // panic

// good: リテラルをあらかじめ与えておく
goodMap := map[string]string{}
nilMap["hoge"] = "foo"   // ok

// もちろん初期値の代入もできる
goodMap := map[string]string{
    "hoge": "foo",
    "huga": "bar",
    "piyo": "qux",
}

// make によるキャパシティを指定した初期化も可能
goodMap := make(map[string]string, 10)   // cap = 10

マップのキーに指定できる型は 比較可能 である必要がある。したがって、スライスなどの比較不可能な型は使用できない。

マップに存在しないキーを呼び出した場合、値の型のゼロ値が返される。そのため、この方法ではキーが存在しているかどうかは判別できない。

x := map[string]string{
    "hoge": "foo",
    "huga": "bar",
    "piyo": "qux",
}

fmt.Println(x["mogu"])   // ""

キーが存在しているかを確認するためには、カンマokイディオム が利用できる。値を参照する際に変数をひとつ増やすことで、その変数に「キーが存在しているか」をしめす boolean が返される。

x := map[string]string{
    "hoge": "foo",
    "huga": "bar",
    "piyo": "qux",
}

v, ok := x["mogu"]
fmt.Println(v, ok)   // "" false

値の削除は delete 関数で行う。

x := map[string]string{
    "hoge": "foo",
    "huga": "bar",
    "piyo": "qux",
}

delete(x, "hoge")
fmt.Println(x)   // map[ huga: bar, piyo: qux ]

マップを集合(Set)として使用する

集合(Set)は Go には存在しない。代わりに、マップを使用してエミュレートすることもできる。
キーを値として利用し、値には「集合内に値が存在しているか」を示す真偽値を置くことで、疑似的に集合として運用できる。

// 初期化
set := map[string]bool{}
val := []string{ "hoge", "huga", "piyo" }
for _, v := range val {
    set[v] = true
}

// 値を呼び出すことで、存在しているかを確認できる
fmt.Println(set["hoge"])   // true
fmt.Println(set["mogu"])   // false

構造体(struct)

Go にはクラスがない。代わりに、任意の型のフィールドを持てる 構造体(struct) がある。
type で定義し、あとは型として変数に割り当てできる。

// 定義
type person struct {
    name string
    age int
    pet string
}

// 宣言
steve := person{
    "Steve",
    12,
    "dog",
}

// フィールド名を指定することで、特定のフィールドを初期化できる
// 記載がないフィールドはゼロ値で初期化される
steve := person{
    name: "Steve",
    age: 12,
}

// フィールドへのアクセスは . で行う
fmt.Println(steve.age)   // 12

また、無名構造体 も定義できる。型として定義しない、その場限りの構造体として使用する。

// 基本形
var person struct {
    name string
    age int
}
person.name = "Steve"
person.age = 12

// または
var person struct {
    name string
    age int
}{
    name: "Steve",
    age: 12,
}

無名構造体は json の操作などによく用いられる。構造体を外部データに変換する処理を マーシャリング(marshaling)、その逆を アンマーシャリング(unmarshaling) と呼ぶ。

構造体の比較・代入・変換

構造体の比較・代入・型変換には、以下のすべての条件がそろっていることが求められる。

  • フィールドの名前がすべて同一
  • フィールドの型がすべて同一
  • フィールドの順番が同一
    そうでない構造体は比較できない。
よつよつ

4. ブロック、シャドーイング、制御構造

Go におけるブロックによる変数のスコープ、それに関する変数参照の挙動(シャドーイング)、またいくつかの制御構文(if-else, for, switch)について触れる。

ブロック

多言語同様、変数にはスコープが存在する。これを制御するのがブロックである。

  • 関数外で宣言した変数はパッケージブロックに置かれる。この変数には、同パッケージであれば別ファイルからでもアクセスできる。
  • import によってインポートされた変数はファイルブロックに置かれる。同ファイルからは参照できるが、別ファイルからは参照できない。
  • このほか、組み込みの型や関数名(ex. int, make)はユニバースブロックに置かれる。Go はキーワードを 25 個しか持たず、組み込みの機能はこちらに定義される。

ブロックは {} で定義できる。

{
    x := 10
    fmt.Println(x)   // 10
}
fmt.Println(x)   // panic

シャドーイング

ブロック外で定義・代入した変数にブロック内で再代入を行い、ふたたびブロック外で参照する場合を考える。このとき、ブロック内で代入された値はブロック内で破棄され、最後にブロック外で参照する際には最初に代入した値が参照される。この挙動を「シャドーイング(隠ぺい)」と呼ぶ。

x := 5
{
    x := 10
    fmt.Println(x)   // 10
}
fmt.Println(x)   // 5
```go

インポートされた識別子についても同様。

```go
import "fmt"

func main() {
    fmt := "hoge"
    fmt.Println(fmt)   // panic
}

if

基本は多言語と同じ。

  • 条件を () で囲まない
  • 条件部分に ; 区切りで変数定義を記述できる
    • ここで定義した変数は、if ブロック及び後続する else if, else ブロック内で有効
// 基本
x := "hoge"
if x == "hoge" {
    fmt.Println("huga")
} else {
    fmt.Println("piyo")
}
    
// 変数定義を含める
if n := rand.Intn(10); n%2 == 0 {
    fmt.Println("hoge")
} else if n%3 == 0 {
    fmt.Println("huga")
} else {
    fmt.Println("piyo")
}

for

4 種類存在する。

// 標準形式
for i := 0; i < 10; i++ {
    fmt.Println("piyo")
}

// 条件のみ
i := 0;
for i < 10 {
    i++;
    fmt.Println("piyo")
}

// 無限ループ
i := 0;
for {
    i++;
    fmt.Println("piyo")
    if i > 10 {
        break;
    }
}

// for-range
// 値はすべてコピーである点に注意
l := []int{ 10, 20, 30 }
for i, n := range l {
    fmt.Println(i, n)
}

// 0 10
// 1 20
// 2 30

for-range 構文に関して、文字列もイテレートできる。このとき、マルチバイト文字もよしなに対処してくれる。
また、インデックスが必要ない場合は _ としておくとよい。この構文は、代入時に不要な変数定義を省略する目的でよく使用される。

s := "こんにちは、世界!"
for _, c := range s {
    fmt.Println(string(c))
}

// こ
// ん
// に
// ち
// は
// 、
// 世
// 界
// !

マップのイテレーションも同様に行える。このとき、以下の点に注意。

  • マップをイテレートする場合、インデックスの代わりにキーが与えられる。
  • セキュリティ上の観点から、マップのイテレート順序は毎回ランダムになっている。

ラベル

二重の for ループを考える。内側の for で条件を満たしたとき、外側の for も同時に抜けたい。
こうしたとき、Go では ラベル を使用することで完結に記述できる。

// ラベル定義
outer:
for ... {
    for ... {
        // break または continue 時にラベル名を書くことで、抜け出すことができる
        break outer
    }
}

switch

Go の switch では、デフォルトでフォールスルーが無効になっている(=該当する case があった際、それ以降の case の処理を自動で呼ぶことはない)。そのため、各 case の最後に break を記述する必要はない。
Go の switch は、比較対象の変数をあらかじめ指定しておく場合と、switch には指定せず、case に比較式を書く場合(=ブランクswitch)が存在する。

// 基本形
x := rand.Intn(10)
switch x {
    case 0, 1, 2, 3:
        fmt.Println("small")
    case 4, 5, 6:
        fmt.Println("medium")
    default:
        fmt.Println("large")
}

// ブランクswitch
x := rand.Intn(10)
switch {
    case x < 3:
        fmt.Println("small")
    case x < 6:
        fmt.Println("medium")
    default:
        fmt.Println("large")
}
よつよつ

関数

// 基本形
func hogeFunc(n int, s string) { ... }

名前付き引数・オプション引数

名前付き引数およびオプション引数は存在しない。これを使用する場合、引数型を構造体としてあらかじめ定義する方法が挙げられている。

// 定義
type PersonOpts struct {
    name string
    age int
    pet string
}
func registerPerson(person PersonOpts) { ... }

// 利用
registerPerson(PersonOpts {
    name: "Tanaka",
    age: 22,
    pet: "Pochi",
})

registerPerson(PersonOpts {
    name: "Yamada",
    age: 31,
})

可変長引数

引数の型に ... をつけることで、任意の数の引数を取ることができるようになる。
値はスライスに格納される。

// 定義
func sumAll(vals ...int) int {
	var result int
	for _, v := range vals {
		result += v
	}
	return result
}

// 使用
v := sumAll(1, 2, 3, 4, 5)
fmt.Println(v) // 15

// スライスも渡せるが、展開しておく必要がある
x := []int{10, 20, 30}
v := sumAll(x...) 
fmt.Println(v)   // 60

複数の戻り値

() で囲うことで複数の値を返すことができる。
受け取る場合は個別に変数を用意する必要がある。引数の一部が必要ない場合は _(ブランク識別子) にしておくことでコンパイル時エラーを回避できる。

// 定義
func myFunc() (string, error) { ... }

// 使用
s, err := myFunc()

// 不要な帰り値は _ にしておく
s, _ := myFunc()

名前付き戻り値

変数定義時、戻り値に名前を付けることができる。この変数は関数内の処理で利用するものであり、返り値を受け取る変数名を強制するものではない。

// 定義
func myFunc(id int) (name string, age int) {
    // 定義した戻り値は、処理の結果を記録するように使う
    name, age = findPerson(id)

    return name, age
}

// 使用
// 帰り値を受け取る変数名は、必ずしも戻り値と一致する必要はない
myName, myAge := myFunc()

返り値として扱う変数を事前に定義しておくことを目的としており、この値を返すことを言語使用で要求するものではない。そのため、名前付き戻り値とは違う値を return してもコンパイルエラーにならないので注意。
さらに、この仕様によって 空の return でもコンパイルエラーが発生しない。これは「ブランク return」と命名されており、名前付き戻り値を定義した状態でブランク return をすると、最後に名前付き戻り値に代入した値が自動的に返される。これはコードの可読性を損なうため、推奨されない。

// 名前付き戻り値が返されないパターン
func myFunc(id int) (name string, age int) {
    name, age = personList(id)

    // 別に名前付き戻り値を返す必要はない...が、これは問題になりやすい
    return "Tanaka", 21
}

// 暗黙的に名前付き戻り値が返されているパターン
func myFunc(id int) (name string, age int) {
    name, age = personList(id)

    // name, age が自動的に返される...
    // が、その値が何かは関数内の処理をトレースしないとわからない
    return 
}

関数を値として扱う

基本型や合成型と同様、関数も値として扱うことができる。

// 型(シグネチャ)を定義する
type opFuncType func(n1 int, n2 int) int

func add(n1 int, n2 int) int { ... }
func sub(n1 int, n2 int) int { ... }
func mul(n1 int, n2 int) int { ... }
func div(n1 int, n2 int) int { ... }

// 関数をマップの値として扱う
opMap := map[string]opFuncType {
    "+": add,
    "-": sub,
    "*": mul,
    "/": div,
}

クロージャ

関数 A 内で定義された関数 B のことを クロージャ(closure) と呼ぶ。
関数内の特定の処理を関数化する用途のほか、

  • 引数として関数を受け取る
  • 帰り値として関数を返す
    パターンがある。

このような「関数を受け取る」「関数を返す」関数のことを 高階関数 とよぶ。

関数引数

引数として関数を受け取ることができる。わかりやすいのは sort.Slice など。
引数として受け取った関数をもとにスライスの各要素を評価し、並べ替える。

sort.Slice(people, func(i int, j int) bool {
    return people[i].point < people[j].point
})

関数を返す

帰り値として関数を指定することもできる。

func getCalculator(op string) func(int, int) int {
    return opMap[op]
}

defer

リソースの close 命令などの後始末処理を定義するとき、defer が使用できる。
関数の任意の場所で記述でき、関数の終わりに行う処理を定義できる。なお、defer で定義された処理は LIFO (後入れ先出し) で処理される。

f, err := os.Open(os.Args[1])
if err != nil {
    log.Fatal(err)
}
defer f.Close()   // 関数の処理が最後まで終わってから実行される

defer で指定した関数内から、外側の関数の戻り値を参照することもできる。これには名前付き戻り値を使用する。
無名関数と組み合わせることで、戻り値の結果に応じた後処理ができる。

func insert(ctx context.Context, db *sql.DB, value1, value2 string) (err error) {
    tx, err := db.BeginTx(xtx, nil)
    if err != nil {
        return err
    }
    // err の中身を検証し、内容によって処理を変える
    defer func() {
        if err == nil {
            err = tx.Commit()
        } else {
            tx.Rollback()
        }
    }
    ...
}

よくあるパターンとして、後始末処理をまとめた関数を戻り値として返しておき、呼び出し下で後始末させる方法である。こうすることで、参照元では戻り値を使用しなければいけなくなり、defer の記述を強制できる。

// 定義
func getFile(name string) (*os.File, func(), error) {
    file, err := os.Open(name)
    if err := nil {
        return nil, nil, error
    }

    return file, func() {
        file.Close()
    }, err
}

// 呼び出し
// closer は呼び出さないとコンパイルエラーになるので、defer 忘れに気づける
f, closer, err := getFile(os.Args[1])
if err := nil {
    log.Fatal(err)
}
defer closer()
よつよつ

6. ポインタ

ポインタとは 「値を格納するアドレスを指す値をもった変数」 である。
変数定義時に型を定義したとき、メモリ上に型が指定したサイズのスペースが確保される。ここに代入された値を格納することになる。
なお、 Go でポインタを使用することはあまり推奨されていない。 後続するいくつかの機能に付随する形で使用することが多く、それ以外での使用は Go の性質(後述)上推奨されていない。

// 基本
var x int32 = 1234 // メモリ上に 4バイト(32ビット)のスペースがとられ、'1234' という整数値が格納される
var px *int32 = &x // & でポインタを取り出せる
fmt.Println(x, px) // 1234 0xc000010070

// ポインタ変数の頭に * をつけるとアドレス先の値を取り出せる(デリファレンス)
fmt.Println(*px) // 1234

// 空のアドレスは nil で表現される
// 型に * をつけるか、new 関数で初期化できる
var px *int32
px := new(int32)

合成型のゼロ値が nil なのは ポインタで実装されているから である。合成型を初期化したとき、値を置くためのスペースと、その先頭を示すアドレス(あと長さとキャパシティ)を持ったスペースがそれぞれ初期化され、代入先には後者が置かれる。
なお、基本型リテラルおよび定数からはポインタが取り出せない。これらはコンパイル時に消える値であり、実行時には存在しないからである。これらからポインタを取り出す必要がある場合、

  1. いちど変数に代入する
  2. ポインタを返す関数を実装する
    のどちらかで対応できる。
p := &"hoge" // panic

s := "hoge"
ps := &s // ok

func strp(s string) *string {
    return &s
}
p := strp("hoge")  // ok

Go のガベージコレクション

Go の型は、事前にデータが持つことができる最大のサイズを把握できるようになっている。これにより、コンパイル時に必要とするメモリサイズが正確にわかるようになっている。
これは Go がスタック領域内にできる限りデータを保存し、ガベージコレクションの仕事を減らすためである。Go ランタイムが使用するガベージコレクタは遅延時間の低減を優先しており、これを実現するために関連性のあるデータを連続的に配置したりしている。
ポインタを多く使用する場合、このメリットを打ち消すことになる。メモリ内のあちこちにアクセスすることになり、Go が得意とするシーケンシャルな呼び出しが行えなくなる。

よつよつ

7. メソッド・インタフェース

Go では 継承(inheritance)よりも 合成(composition) を推奨している。
すべての型は 抽象型 もしくは 具象型 のいずれかに属する。

基底型

Go のすべての型は、その型のベースになる 基底型 をもっている。ルールは以下の通り。

  • 基本型あるいは型リテラルの場合、規定型はそれ自身になる
  • それ以外の場合、基底型は宣言時に参照している型になる

メソッド

型に対して関数を定義することで、その型の変数からしか呼び出せない関数が作成できる。これを メソッド という。
関数定義時に レシーバ(p Person))を付記することでメソッドを定義できる。

// 定義
type Person struct {
	name string
	age  int
}

func (p Person) toString() string {
	return fmt.Sprintf("%s (%d)", p.name, p.age)
}


// 利用
p := Person{
    name: "Jon",
    age:  21,
}
fmt.Println(p.toString())   // Jon (21)

レシーバには 値レシーバ および ポイントレシーバ を選択できる。両者の違いは「レシーバ(呼び出し元の変数)を変更するか」であり、変更させたくない場合は前者、変更したい場合は後者を選ぶ。
本書ならびに A Tour of Go などでは、ポインタレシーバを使用するメソッドが1つでもあれば、すべてのメソッドでポインタレシーバを使用するのがよいとされる。値レシーバは関数ごとに変数をコピーするため、メモリ効率的に非効率なためだと思う。

type Person struct {
	name string
	age  int
}

func (p Person) toString() string {
	return fmt.Sprintf("%s (%d)", p.name, p.age)
}

func (p Person) updateWrong(name string) {
	p.name = name
}

func (p *Person) updateRight(name string) {
	p.name = name
}

func main() {
	p := Person{
		name: "Jon",
		age:  21,
	}
	fmt.Println(p.toString())   // Jon (21)

	p.updateWrong("hoge")
	fmt.Println(p.toString())   // Jon (21)

	p.updateRight("huga")   
	fmt.Println(p.toString())   // huga (21)
}

ほとんどの場合、ポインタレシーバを使用することが推奨される。値レシーバは呼び出し対象が nil だった場合パニックを起こすが、ポインタレシーバは(処理さえ問題なければ)正常に動作する。
逆に言えば、ポインタレシーバを使用する場合はレシーバが nil である場合を十分に考慮する必要がある。

func (p Person) updateWrong(name string) {
	p.name = name
}

func (p *Person) updateRight(name string) {
	if p == nil {
		fmt.Println("it's nil")
		return
	}
	p.name = name
}

func main() {
	var p *Person = nil
	p.updateWrong("hoge") // panic
	p.updateRight("huga") // it's nil
}

定義したメソッドは、インスタンスから取り出して変数に格納したり(メソッド値)、型自体から取り出して変数に格納したり(メソッド式)できる。これは一般には使わないが、後述するインタフェースの DI などで使用する。

type Person struct {
	name string
	age  int
}

func (p *Person) Update(name string) {
	p.name = name
}

func main() {
	p := Person{
		name: "Jon",
		age:  21,
	}

	// メソッド値として扱う
	f1 := p.Update
	f1("Kriss")
	fmt.Println(p) // {Kriss 21}

	// メソッド式として扱う
	f2 := (*Person).Update
	f2(&p, "Alice")
	fmt.Println(p) // {Alice 21}
}

型宣言と継承

ユーザー定義型をもとに、新たなユーザー定義型を定義することができる。このとき、基底型は元のユーザー定義型から引き継がれる。

type Score int
type MyScore Score // 基底型は int

これは継承の概念とは異なる。継承ではクラス間に親子関係が成立し、親となる型の変数に子のインスタンスを代入できたりするが、Go では不可能である。代入は型変換を通す必要があり、さらに、ユーザ定義型を参照して定義された型は、参照先のメソッドなどを引き継がない。

type Person struct {
	name string
	age  int
}
type Friend Person

func (p *Person) Update(name string) {
	p.name = name
}

func main() {
	p := Person{
		name: "Jon",
		age:  21,
	}
	f := Friend{
		name: "Fred",
		age:  23,
	}

	p.Update("hoge") // ok
	f.Update("huga") // panic
}

iota

Go には列挙型(enum)がない。代わりに、連続した定数定義に自動で値を代入してくれる識別子 iota が存在する。

const (
	Zero int = iota
	One
	Two
	Three
)

func main() {
	fmt.Println(Zero, One, Two, Three) // 0 1 2 3
}

iota を使用する際は、各定数につける型を定義し、それを iota とともに使用することが推奨される。

type RarityCategory int

const (
	Common RarityCategory = iota
	Uncommon
	Rare
	SuperRare
	Legendary
)

func main() {
	fmt.Println(Uncommon, Rare) // 1 2
}

iota を使用する場合、個別にリテラルを代入すべきではない。iota を使用した定数の値に意味を持たせるべきではない。変更に弱いコードになる可能性が高い。

埋め込みによる合成

Go では 合成(composition)昇格(promotion) が組み込まれている。
型定義時、任意の型を名前を付けずに書くことで 埋め込みフィールド(embedded field) にできる。こうすることで、埋め込まれた型の持っているメソッドなどにアクセスできるようになる。

type Person struct {
	name string
	age  int
}

func (p Person) Introduce() {
	fmt.Printf("%s です。%d 歳です。", p.name, p.age)
}

type Employer struct {
	Person
	hourlyPay int
}

func (e Employer) IntroduceSalary() {
	fmt.Printf("時給は %d 円です。", e.hourlyPay)
}

func main() {
	e := Employer{
		Person: Person{
			name: "Jon",
			age:  21,
		},
		hourlyPay: 1_300,
	}

	e.Introduce()       // Jon です。21 歳です。
	e.IntroduceSalary() // 時給は 1300 円です。
}

埋め込み元と埋め込み先に同名のフィールドがあった場合、呼び出した際の記述によって参照する値が違う。埋め込み元の値を参照したい場合、それを明記する必要がある。


type Person struct {
	name string
	age  int
}

type Employer struct {
	Person
	name      string
	hourlyPay int
}

func main() {
	e := Employer{
		Person: Person{
			name: "Jon",
			age:  21,
		},
		name:      "Eric",
		hourlyPay: 1_300,
	}

	fmt.Println(e.name)        // Eric
	fmt.Println(e.Person.name) // Jon
}

インタフェース

Go における唯一の中小型として インタフェース が使用できる。
インタフェースは次の2つの側面を持つ。

  1. メソッドの集合:特定のクラスが満たすべき条件(実装すべき一連のメソッド)を示す
  2. 型:インタフェースが示す型の集合のうち任意の型の値を代入できる変数が定義できたりする

Go のインタフェースは 暗黙的に実装される。Java のように型宣言時に明言する必要はなく、条件を満たしていれば自動的にインタフェースを実装したことになる。

package main

import (
	"fmt"
)

// interface
type Letter interface {
	Write(text string)
	Read()
}

// struct
// 暗黙的に Letter を実装する
type LoveLetter struct {
	text string
}

func (l *LoveLetter) Write(text string) {
	l.text += text
}

func (l *LoveLetter) Read() {
	fmt.Println(l.text)
}

func main() {
	var l Letter
	l = &LoveLetter{} // success
	l.Write("Hello World!")
	l.Read() // Hello World!
}

デコレータパターン

Go では デコレータパターン を使うことが推奨されている。これは、インスタンスに対して動的にふるまいを追加していく書き方である。
インタフェースとデコレータパターンを使った一例を示す。ここで os.Open から返される ros.File のインスタンスであり、 io.Writer io.Reader をそれぞれ満たす。しかし、そのままだと gzip で圧縮されたファイルの読み込みができないので、gzip.NewReader によって圧縮ファイルを読み込む振る舞いを追加している。返される gz も変わらず io.Reader io.Writer を実装している。

// ファイルを読み込むためのインスタンス r を生成
r, err := os.Open(fileName)
if err != nil {
    return err
}
defer r.Close()

// そのままでは gzip 圧縮されたファイルを読み込めない
// ので、ファクトリ関数 gzip.NewReader(r) によってふるまいを加える
gz, err = gzip.NewReader(r)
if err != nil {
    return err
}
defer gz.Close()

// ...

インタフェースの合成

インタフェースも合成が可能である。

type Gum interface {
    Stick()
}

type Rubber interface {
    Stretch()
}

type BungeeGum interface {
    Gum
    Rubber
}

インタフェースを受け取り構造体を返す

基本的に、引数はインタフェースで渡されるべきであり、返り値は構造体で返されるべきである。これはコードの変更に対して柔軟性を持たせるための書き方であり、引数に新しいパターンの型を渡したくなっても(インタフェースさえ満たしていれば)渡せること、逆にインタフェースを返してしまうと呼び出し先のコードの依存関係を複雑にしてしまう(cf. デカップリング)ことからこのような習慣ができている。
ただし、引数をインタフェースにするのにはパフォーマンス的な問題が伴う。インタフェース引数を受け取った時、各インタフェース引数はヒープに保存されるため GC の仕事量が増える(構造体であればスタックにコピーされる)。インタフェースを受け取り変更に柔軟な構造にするか、構造体を受け取ってパフォーマンス性能の高い構造にするかは場合によって使い分ける必要がある。

インタフェースとゼロ値

インタフェースのゼロ値は nil である。が、インタフェースが nil であるためには、

  • ベースとなる型(基底型)へのポインタを持たない
  • ベースとなる値へのポインタを持たない
    の両方を満たす必要がある。
type Empty interface{}

func main() {
	var e Empty
	fmt.Println(e == nil) // true

	var s *string
	e = s
	fmt.Println(e == nil) // false
}

空インタフェース

interface{} および any (Go 1.18以降)で、Go の任意の型を満たすことを表現できる。これは、形式が不明な JSON などの記憶場所として用いることが多い。

data := map[string]interface{}{}
// または
data := map[string]any{}

無論 Go は強い型付け言語としてデザインされているため、むやみな使用はイディオム的ではない。

型アサーション

インタフェース型の変数が、別の具象型あるいはインタフェースを満たしているかチェックするためには、型アサーション が使用できる。

// 比較対象とするインタフェース
type NicePerson interface {
	Introduce()
}

// 比較対象を満たす型
type NiceGuy struct {
	name string
}

func (n NiceGuy) Introduce() {
	fmt.Printf("Howdy! I'm %s!\n", n.name)
}

// 比較対象を満たさない型
type Nerd struct {
	name string
}

func main() {
    // 今回は比較のため、型を明示せず(any 型のインタフェースで定義して)代入する
	var p1, p2 any
	p1 = NiceGuy{name: "Alex"}
	p2 = Nerd{name: "Bob"}
	fmt.Println(p1, p2) // {Alex} {Bob}

    // success
	np1, ok := p1.(NicePerson)
	if !ok {
		err := fmt.Errorf("They is not nice person: %v\n", p1)
		fmt.Println(err.Error())
		os.Exit(1)
	}
	np1.Introduce() // Howdy! I'm Alex!

    // failure
	np2, ok := p2.(NicePerson)
	if !ok {
		err := fmt.Errorf("They is not nice person: %v\n", p2) // They is not nice person: {Bob}
		fmt.Println(err.Error())
		os.Exit(1)
	}
	np2.Introduce()
}

型 switch

switch 文を使用して、値の型をもとに処理を分岐させることができる。

func Introduce(p any) {
	switch p := p.(type) {
	case nil:
		fmt.Println("person not found (nil)")
	case NiceGuy:
		p.Introduce()
	case Nerd:
		fmt.Printf("Hi, I'm %s.\n", p.name)
	default:
		fmt.Println("p is probably not person")
	}
}

func main() {
	var p1 any
	p2 := NiceGuy{name: "Alex"}
	p3 := Nerd{name: "Bob"}
	p4 := 123

	Introduce(p1) // person not found (nil)
	Introduce(p2) // Howdy! I'm Alex!
	Introduce(p3) // Hi, I'm Bob.
	Introduce(p4) // p is probably not person
}

一般に、型アサート及び switch は頻繁に使用されるべきではない。変数や引数の型を、付随した型以外にとらえて処理をした場合、定義した際の型かアサート以降の処理どちらかに問題があることになる。

これらを使用すべきタイミングとして、「実装されているかもしれないインタフェースをチェックする」場合が挙げられる。io.Copy は引数として io.Writerio.Reader の2つを受け取って Writer に内容をコピーする関数だが、引数として渡された値が io.WriterToio.ReaderFrom を実装していた場合、それらを直接使用することでメモリパフォーマンスを向上させる。
型 switch はあくまで「渡される型が特定の型に限られる」場合にのみ使用すべきであり、それ以外の型は想定外として default でエラーを返すなどするべきである。

関数をインタフェースの実装として扱う

関数 F1 を持つインタフェースと、実装された関数 F2 があるとする。インタフェースを実装したいとして、F1 の実装に F2 を使用したい場合、構造体を作成し、そこに F2 を関数として紐づける方法が一般的である。
しかし、Go では任意のユーザ定義型に対してメソッドが追加できる ようになっている。これは関数も例外ではなく、「関数をインタフェースの実装にする」用に書くことで、構造体を作成することなくインタフェースの実装ができる。

// ここに、絵師を示す Drawer というインタフェースと
type Drawer interface {
    Draw(req string) image.Image
}
// インタフェース内の関数の定義を満たす実装 DrawFunc がある
func DrawFunc(req string) image.Image {
    // ...
}
// この関数をもとに、Drawer を満たす構造体をつくりたい


// 1. 構造体を用意し、そこに実装として DrawFunc をわたす
type MyDrawer struct {}
func (d MyDrawer) Draw(req string) image.Image {
    DrawFunc(req)
}

// 2. 関数を直接 Drawer として扱う
// こっちのほうが記述量が少ないのでよいとされる
func (d DrawFunc) Draw(req string) image.Image {
    DrawFunc(req)
}

これはメソッドをひとつしか持たないインタフェースの実装に有用である。

依存性注入(DI)

Go の暗黙的なインタフェースの利点の一つとして、DI が行いやすいことにある。ほかの言語のように追加のライブラリを必要とせず、実装が容易である。

// logger の定義+実装
type Logger interface {
	Log(s string)
}
type LoggerAdapter func(s string)

func (lg LoggerAdapter) Log(s string) {
	lg(s)
}

// (MyLog は LoggerAdapter を暗黙的に実装する = Logger も暗黙的に実装している)
func MyLog(s string) {
	fmt.Println("[ T_T]< ", s)
}

// calculator の定義+実装
type Calculator interface {
	Add(n1, n2 int) int
}
type CalculatorAdapter func(n1, n2 int) int

func (cl CalculatorAdapter) Add(n1, n2 int) int {
	return cl(n1, n2)
}

// (MyAdd は CalculatorAdapter を暗黙的に実装する = Calculator も暗黙的に実装している)
func MyAdd(n1, n2 int) int {
	return n1 + n2
}

// ロジックではインタフェースで関係を示し、依存はしない
type CalcLogic struct {
	l Logger
	c Calculator
}

func (cl CalcLogic) Exec(n1, n2 int) int {
	cl.l.Log("Logic execute")
	n := cl.c.Add(n1, n2)
	return n
}

// main で呼び出す際に Logic に実装を渡す
func main() {
	// ここでキャストする必要がある
	logger := LoggerAdapter(MyLog)
	calculator := CalculatorAdapter(MyAdd)

	logic := CalcLogic{
		l: logger,
		c: calculator,
	}
	result := logic.Exec(10, 20)
	fmt.Printf("result: %d", result)

    // [ T_T]<  Logic execute
    // result: 30    
}
よつよつ

8. エラー処理

Go では例外処理を行うための機能は存在しない。かわりに errors パッケージによって生成される error を関数の帰り値とすることでエラーを表現する。

// エラーを示すインタフェース
type error interface {
    Error() string
}

// 処理でエラーが起きた際は error を返す
// 正常に処理された場合は、error = nil を返す
func Divide(n1, n2 int) int {
    if n2 == 0 {
        return 0, errors.New("n2 is 0")
    }
    return n1 / n2
}

func main() {

// 呼び出し側では、エラーに対して2種類の対応ができる
// 1. エラーを if 文でチェックし処理する
n, err := Divide(100, 0)
if err != nil {
    fmt.Println(err)
    os.Exit(1)
}

// 2. エラーを明示的に無視する
n, _ := Divide(100, 0)
}

エラーに変数を埋め込みたい場合は fmt.Errorf が使用できる。
受け取った文字列に変数を埋め込み、error オブジェクトを生成する。

return 0, fmt.Errorf("%d は偶数ではありません", n)

センチネルエラー

処理を開始・継続できないことを示すためのエラーを センチネルエラー と表現する。
これを処理するため、パッケージレベルで変数として error を定義し、public API として公開する場合がある。ZIP ファイルを読み込む archive/zip における、フォーマットが指定された形式ではないエラー ErrFormat などがこれにあたる。
呼び出し下では、エラーチェック時にセンチネルエラーに対するチェックが必要になる。

func main() {
    data := []byte("This is not a zip file")
    nonZipFile := bytes.NewReader(data)
    _, err := zip.NewReader(nonZipFile, int64(len(data)))
    if err := zip.ErrFormat {
        fmt.Println("Zip 形式ではありません")
    }
    // ...
}

エラーと値

error はインタフェースなので、任意の拡張ができる。

type StatusErr struct {
    Status string
    Message string
}
func (se StatusErr) Error() string {
    return se.Message
}

エラーのラップ

例えば関数 myFileOpen を定義したとして、内部で os.Open のエラーを拾ったとする。これをクライアント側にエラーとして返す場合、os.Open のエラーも文脈に含めたい。
このようにエラーに文脈を足したい場合、エラーのラッピング が使用できる。

fmt.Errorf には %w という動詞があり、これはエラーを受け取って文に埋め込むことができる。
このようにエラーにエラーを含めて返すことで、エラーが発生する一連の流れ(=エラーチェーン)が返されるようになり、デバッグの難易度が下がることがある。

func myFileOpen(path string) error {
    f, err := os.Open(name)
    if err != nil {
        fmt.Println(err)
        return fmt.Errorf("in myFileOpen: %w", err)
    }
}

func main() {
    err := myFileOpen("not-exist.txt")
    if err != nil {
        fmt.Println(err)
        if wrappedErr := errors.Unwrap(err); wrappedErr != nil {
            fmt.Println(err)
        }
    }
}