Go言語は、静的型付け・ガベージコレクション・組み込みの並行処理を備えたコンパイル言語です。Googleによって開発され、2009年に公開されました。
言語の特徴
ソースはコンパイルしてから実行し、型の不整合はビルド時に検出されます。メモリはランタイムのガベージコレクションが回収するため、ヒープの解放をプログラマが担う必要はありません。ビルド結果は依存ライブラリを同梱した単一バイナリになり、仮想マシンなしで配布できます。
並行処理向けに、ゴルーチンとチャネルが言語に組み込まれています。ゴルーチンはOSスレッドより起動コストが小さく、多数のゴルーチンを並行に走らせやすい設計です。失敗は例外ではなくerror型の戻り値で明示的に扱います。クラス継承はなく、構造体の埋め込みとインターフェースで振る舞いを組み立てます。
公式ドキュメント
ツールチェーンのインストール
公式のダウンロードページから、利用しているOS向けのインストーラまたはアーカイブを入手してインストールします。
ターミナルを開き、go versionを実行してツールチェーンが導入されたか確認します。
go version次のようにバージョン情報が表示されれば、正しくインストールされています。正しくインストールされていない場合はcommand not foundなどのエラーが出ます。
go version go1.27.1 windows/amd64最新の安定版情報はリリース履歴で確認できます。
プロジェクトの作成とモジュールの初期化
Go言語では、プログラムをモジュールという単位で管理します。プロジェクト用のディレクトリを作成し、その直下でgo mod initを実行してモジュールを初期化します。
mkdir hello
cd hello
go mod init example.com/hello実行すると、プロジェクトのルートにgo.modが作成されます。初期状態ではモジュール名とGo言語のバージョンが書かれ、外部パッケージを追加すると依存もここに記録されます。
Go言語1.16以降はモジュールモードが標準のため、任意のディレクトリでプロジェクトを開始できます。旧来のGo言語のように、決められた作業領域(GOPATH)の下にソースコードを配置する必要はありません。
Hello, Worldの実行
初期化したディレクトリ内にプログラムファイルを作成し、動作を確認します。ソースファイルのファイル名に言語仕様上の縛りはなく、hello.goなど任意の名前にできますが、エントリポイントを含むファイルには慣習としてmain.goが広く使われます。
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}プログラムを実行するには、ファイル名を直接指定してgo run main.goを実行します。
go run main.goカレントディレクトリを指定するgo run .でも同様に実行できます。末尾の.はカレントディレクトリのパッケージ全体を対象にすることを表します。ファイルが1つだけのときはどちらでも動作します。
go run .ターミナルにHello, Worldと出力されれば、コンパイルと実行が正常に行われています。
プログラムの基本構成
実行可能なプログラムには、package mainの宣言と、引数も戻り値もないfunc main()が必要です。
先頭のpackage mainが、このパッケージがエントリポイントであることを示します。他パッケージを使うときはimportで取り込みます。上の例では標準ライブラリの入出力のためにimport "fmt"を書き、func main()の中でfmt.Printlnを呼んでいます。
Go言語では、使っていないimportや、関数内で宣言したまま使っていない変数があるとコンパイルエラーになります。
複数ファイルでの構成
同一ディレクトリにある複数の.goファイルは、同じpackage句を書くことで1つのパッケージとしてまとまります。別ファイルで定義した関数も、同じパッケージ内であればそのまま呼び出せます。
たとえばgreet.goに関数を定義し、main.goのfunc main()から呼び出す例です。
// greet.go
package main
func greet(name string) string {
return "Hello, " + name
}// main.go
package main
import "fmt"
func main() {
fmt.Println(greet("Go言語"))
}複数ファイルがある環境でgo run main.goのように単一ファイルのみを指定すると、他のファイルがコンパイル対象に含まれず、undefined: greetのような未定義エラーになります。ファイルを並べてgo run main.go greet.goと実行することもできますが、ファイルが増えるごとに指定が必要になるため、日常的にはカレントディレクトリのパッケージ全体をまとめてビルド・実行するgo run .を使用します。
基本的なコマンド
go run
一時的にコンパイルしてプログラムを即座に実行します。開発中にすぐ動かしたい場合に使います。
go run .go build
実行可能バイナリを生成します。配布やデプロイ用のファイルを書き出すときに使います。-oオプションで出力ファイル名を指定できます。省略した場合は、モジュール名の末尾(この例ではhello)が実行ファイル名になります。
go build -o hello .go fmt
Go言語の標準スタイルに従ってソースコードを自動整形します。インデントや改行位置が一貫した形式に整います。ディレクトリ配下を一括で整形するときは./...を指定します。
go fmt ./...go test
パッケージ内のテストコード(_test.go)を検出して実行します。
go test ./...ソースコードの表現
Go言語のソースコードはUTF-8でエンコードされたUnicodeテキストです。大文字と小文字は別の文字として区別されます。
コメント
コメントは、行コメント(//)とブロックコメント(/* */)の2種類です。入れ子にはできません。
// 行コメント(行末まで)
/*
ブロックコメント(複数行)
*/改行を含まないブロックコメントは空白1つとして扱われます。一方、行コメントや改行を含むブロックコメントは改行として扱われます。
識別子とキーワード
識別子は、変数、関数、型、パッケージなどに付ける名前です。先頭は文字で始まり、2文字目以降には数字も使えます。文字にはUnicodeの文字とアンダースコア_が含まれます。
アンダースコア_は小文字として扱われるため、識別子の先頭に置くこともできます。先頭が小文字または_の名前はパッケージ外から参照できません。外部に公開されるのは、先頭が大文字の名前だけです。
var _count int // 先頭が_なのでパッケージ外からは見えない
var count int // 先頭が小文字でも同様に見えない
var Count int // 先頭が大文字なら他パッケージから見える次の25語は構文を定義するためのキーワードであり、識別子には使えません。
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var一方、true、false、nilや型名、組み込み関数はキーワードではなく事前宣言された識別子です。キーワードとは異なり、同名の変数として再定義できてしまいます。
true := 1
// var if int = 1 // コンパイルエラーセミコロン
Go言語の構文上、文末にはセミコロンが必要です。ただし、字句解析の段階でコンパイラがセミコロンを自動で補うため、ソースコードに;を直接書く機会はほとんどありません。
行末のトークンが次のいずれかである場合、その直後にセミコロンが自動挿入されます。
- 識別子
- リテラル
break、continue、fallthrough、return++、--、)、]、}
明示的に書いた;もそのまま解釈され、行末にあっても二重に挿入されることはありません。
また、閉じ括弧)や}の直前ではセミコロンを省略できます。改行がない1行コードでも、末尾の括弧の手前に;を書かずに済みます。
func inc(x int) int { return x + 1 }改行を含むブロックコメントは改行として扱われるため、直前のトークン次第でセミコロンが自動挿入されます。
a = b /* 改行を含むコメントは改行扱いになる
*/ + c // コンパイルエラー: a = b; + c と解釈されるifやforなどの制御構文では開き括弧{を次の行に置くことができません。条件式の直後で改行するとセミコロンが補われてしまい、構文エラーになります。そのため、開き括弧{は同じ行に書きます。
if x < 0 {
return -x
}if x < 0
{
// コンパイルエラー: if x < 0; と解釈される
}変数宣言
varで型と初期値を明示できます。型を省略すると右辺から推論します。関数内では:=による短い変数宣言も使えます。
var x int = 10
var y = 20 // 型推論でint
z := 30 // 短い変数宣言(関数内のみ)パッケージ直下では:=は使えず、varが必要です。
package demo
var count = 0 // OK
// n := 1 // コンパイルエラー: パッケージレベルでは:=不可宣言した変数を使わないとコンパイルエラーになります。意図的に捨てるときは_へ代入します。
x := 1
_ = x同じブロック内で複数の変数をまとめられます。
var (
host = "localhost"
port = 8080
)複数の変数に同時代入することもできます。関数や一時変数を使わなくても変数の値を入れ替えることもできます。
func swap(a, b int) (int, int) { return b, a }
x, y = 1, 2
x, y = swap(x, y) // 2, 1
x, y = y, xゼロ値
Go言語には未初期化のローカル変数はなく、宣言された変数は常にゼロ値か、明示的な初期化値を持ちます。
| 型 | ゼロ値 |
|---|---|
| 数値 | 0 |
bool | false |
string | "" |
| ポインタ、スライス、マップ、チャネル、関数、インターフェース | nil |
| 構造体 | 各フィールドのゼロ値 |
| 配列 | 各要素のゼロ値 |
nilは使うための内部のデータ構造がまだ用意されていない状態を表します。操作がその内部構造への読み書きを前提にするとパニックになります。内部構造がなくても意味がある操作や、appendなどの必要ならこちらで確保する操作は動きます。
var m map[string]int // m == nil
// m["x"] = 1 // ハッシュ表が無いのでpanic
m = make(map[string]int) // または m = map[string]int{}
m["x"] = 1
var s []int
s = append(s, 1) // 要素を格納する配列を確保して動く定数宣言
定数はコンパイル時に評価できる式に限られます。変数の参照や関数呼び出しは定数式にならず、コンパイルエラーになります。
const Pi = 3.14159
const (
StatusOK = 200
StatusNotFound = 404
)
var limit = 10
// const max = limit // コンパイルエラー: 変数は定数式に使えない
// const n = len([]int{1, 2, 3}) // コンパイルエラー: スライスの長さは定数にならない
// const now = time.Now() // コンパイルエラー: 実行時の値は使えないiota
定数宣言ブロック内でiotaは行ごとに0から増える連番です。新しいconstブロックを開くとiotaは0にリセットされます。
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)式と組み合わせた例です。各行でiotaの値が先に決まり、その行の式に代入されます。
const (
_ = iota // 0を捨てる
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
)オペランドと演算子
オペランドは演算の対象となる値や式であり、演算子はそれらを結びつける記号です。演算子はオペランドの数に応じて二項演算子と単項演算子に分かれます。
二項演算子
二項演算子は2つのオペランドの間に置きます。
| 種類 | 演算子 |
|---|---|
| 算術 | + - * / % |
| ビット | & | ^ &^ << >> |
| 比較 | == != < <= > >= |
| 論理 | && || |
a, b := 1, 2
sum := a + b // 3
masked := a & b // 0
ok := a < b // true
both := true && ok // true単項演算子
単項演算子はオペランドの直前に置きます。Go言語では前置のみをサポートしています。
| 演算子 | 意味 |
|---|---|
+ | 値はそのまま |
- | 正負の反転 |
! | 真偽値の反転 |
^ | ビットの反転 |
* | ポインタが指す値の参照 |
& | アドレスの取得 |
<- | チャネルからの値の受信 |
pos := +10 // 10
neg := -10 // -10
on := true
stopped := !on // false
flipped := ^uint8(0b00001100) // 0b11110011
var n int = 10
p := &n
v := *p // 10
ch := make(chan int, 1)
ch <- 1
got := <-ch // 1算術演算子
| 演算子 | 意味 | 対象 |
|---|---|---|
+ | 加算、文字列の連結 | 整数、浮動小数点、複素数、文字列 |
- | 減算 | 整数、浮動小数点、複素数 |
* | 乗算 | 整数、浮動小数点、複素数 |
/ | 除算。整数除算はゼロ方向へ切り捨て | 整数、浮動小数点、複素数 |
% | 剰余 | 整数 |
& | ビット積 | 整数 |
| | ビット和 | 整数 |
^ | 排他的論理和 | 整数 |
&^ | 指定ビットの消去。右辺で1のビット位置を0にする | 整数 |
<< | 左シフト。右オペランドは0以上 | 整数 |
>> | 右シフト。右オペランドは0以上 | 整数 |
a, b := 10, 3
sum := a + b // 13
joined := "go" + "lang" // "golang"
diff := a - b // 7
prod := a * b // 30
quot := a / b // 3
negQuot := (-10) / 3 // -3
rem := a % b // 1
x, y := uint8(0b1100), uint8(0b1010)
bitAnd := x & y // 8
bitOr := x | y // 14
bitXor := x ^ y // 6
bitClear := x &^ y // 4
shiftLeft := x << 1 // 24
shiftRight := x >> 1 // 6比較演算子
| 演算子 | 意味 | 対象 |
|---|---|---|
== | 等しい | 比較可能な型 |
!= | 等しくない | 比較可能な型 |
< | より小さい | 順序付け可能な型 |
<= | 以下 | 順序付け可能な型 |
> | より大きい | 順序付け可能な型 |
>= | 以上 | 順序付け可能な型 |
順序付け可能な型は整数、浮動小数点、文字列です。boolやポインタは等値比較はできても順序比較はできません。
スライス、マップ、関数は値同士を比較できませんが、いずれもnilとの等値比較は可能です。配列は要素型が比較可能であれば==で比較できます。
x, y := 1, 2
isLess := x < y // true
isLessOrEqual := x <= y // true
isGreater := x > y // false
isGreaterOrEqual := x >= y // false
isEqual := x == y // false
isNotEqual := x != y // true
// []int{1} == []int{1} // コンパイルエラー: スライス同士は比較不可
// map[int]int{1: 1} == map[int]int{1: 1} // コンパイルエラー: マップ同士は比較不可
// func() {} == func() {} // コンパイルエラー: 関数同士は比較不可
var pending []int
isNil := pending == nil // true: スライス、マップ、関数はnilと比較可能論理演算子
| 演算子 | 意味 |
|---|---|
&& | 論理積 |
|| | 論理和 |
! | 論理否定 |
&&と||は短絡評価します。&&は左辺がfalseなら右辺を評価しません。||は左辺がtrueなら右辺を評価しません。右辺に関数呼び出しなどの副作用がある場合でも、評価されない限り実行されません。
func sideEffect(n int) int { return n }
active := true
paused := !active // false
if false && sideEffect(2) == 2 {
// 左辺がfalseのためsideEffectは評価されない
}
if true || sideEffect(2) == 2 {
// 左辺がtrueのためsideEffectは評価されない
}++と--
| 演算子 | 意味 |
|---|---|
++ | 1増やす |
-- | 1減らす |
文であり、式としては扱えません。前置の++iはなく、f(i++)のように式のオペランドに含めることもできません。
i := 0
i++
i--
// f(i++) // コンパイルエラー: 文のため式の中では使えない型変換
二項演算子のオペランドは、シフト演算と非型付き定数を除き同一の型でなければなりません。型付きの変数同士に暗黙の数値変換はないため、intとfloat64をそのまま計算するとコンパイルエラーになります。一方のオペランドが非型付き定数なら、相手の型で表現できる値であればその型として使えます。
var x int = 1
var y float64 = 2
// z := x + y // コンパイルエラー: 型の異なる変数同士は演算不可
mixed := float64(x) + y
sum := x + 2 // 非型付き定数2はintとして扱われるifやforの条件式と論理演算子のオペランドは真偽値に限ります。boolのほか、基底型がboolの型も使えます。数値や文字列からboolへの暗黙変換はなく、bool(n)のような明示的な変換もありません。真偽判定には比較式を用います。
n := 1
s := "go"
var err error
// if n { } // コンパイルエラー: 条件式は真偽値が必要
// if s { } // コンパイルエラー
// ok := bool(n) // コンパイルエラー: intからboolへの変換は不可
hasN := n != 0 // true
hasS := s != "" // true
hasErr := err != nil // false定数式
定数式は定数のみで構成され、コンパイル時に計算されます。計算結果は定数の宣言や配列の要素数指定などにそのまま利用できます。
const size = 1 + 2*3
var arr [size]int演算子の優先順位
二項演算子
仕様では優先度1から5まで定義されており、数値が大きいほど強く結合し、先に計算されます。
| 優先度 | 演算子 |
|---|---|
| 5 | * / % << >> & &^ |
| 4 | + - | ^ |
| 3 | == != < <= > >= |
| 2 | && |
| 1 | || |
同じ優先度の二項演算子は左から右へ結合します。たとえば42 + a - bは(42 + a) - bと解釈されます。
n := 1 + 2*3 // 乗算が優先され7になる単項演算子
単項演算子は、すべての二項演算子よりも強く結合します。++と--は文であるため優先順位の定義には含まれません。
x := uint8(0b1100)
n := ^x >> 1 // 単項演算の^xが優先されるため121評価順序
関数呼び出し、メソッド呼び出し、チャネル受信、二項論理演算は、記述された順序に従って左から右へ評価されます。たとえばf() + g()ではf()が先に実行されてからg()が実行されます。関数の引数同士も同様で、sub(next(), next())では左側のnext()が先に呼び出されます。
一方、変数の参照と関数呼び出しの相対的な評価順序は仕様で定められていません。
func f() int { return 1 }
func g() int { return 2 }
sum := f() + g() // f()の実行後にg()が実行され3になる
var seq int
func next() int {
seq++
return seq
}
func sub(a, b int) int { return a - b }
sub(next(), next()) // 左辺が1、右辺が2となり常に-1
n := 1
inc := func() int { n++; return n }
pair := []int{n, inc()} // nの参照とinc()の実行順序が未規定のため[1, 2]または[2, 2]ブロック
ブロックは宣言と文のグループです。波括弧{ }で囲みます。関数本体、制御構文の本体、ifの初期化文を含む全体などがブロックを形成します。
スコープ
識別子は宣言されたブロックとその内側のブロックで有効です。内側のブロックで同名を宣言すると、外側の名前はそのブロック内では隠れます。
x := 1
y := 2
{
fmt.Println(y) // 2
x := 3
fmt.Println(x) // 3
}
fmt.Println(x) // 1再宣言と:=
:=は、左辺に少なくとも1つ新しい変数があるときに使います。すでにそのブロックで宣言されている名前には代入され、それ以外は新たに宣言されます。
x, y := 1, 2
x, z := 3, 4 // xは代入、zは新規ラベルのスコープ
ラベルは、Outer:のように文の前に付ける名前です。入れ子のループで外側へ抜けるときはbreak Outer、外側ループの次の反復へ進むときはcontinue Outerと書きます。gotoの飛び先にも使います。
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
break Outer // 内側も外側も抜け、両方のループを終了
}
}
}Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue Outer // 内側を抜け、外側ループの次のiへ
}
}
}i := 0
Again:
fmt.Println(i)
i++
if i < 3 {
goto Again
}ラベルはそれを含む関数内でのみ有効です。別の関数のラベルには飛べません。
基本型
数値型
整数型には符号付きのint8、int16、int32、int64、int、符号なしのuint8、uint16、uint32、uint64、uint、uintptrがあります。
浮動小数点はfloat32とfloat64です。複素数はcomplex64とcomplex128です。
整数リテラルは10進、2進(0b/0B)、8進(0/0o/0O)、16進(0x/0X)を書けます。可読性のため桁の間に_を入れられます。
decimal := 42
binary := 0b101010
octal := 0o52
hex := 0x2A
withSep := 1_000_000浮動小数点リテラルは小数点表記や指数表記を取れます。虚数リテラルは末尾にiを付けます。1のような整数リテラルは、代入先がfloat64やfloat32ならその浮動小数点型として解釈されます。
f := 3.14159
e := 1.5e-10
imag := 2i
var x float64 = 1 // 1はfloat64になる
var y float32 = 2 // 2はfloat32になる論理型
boolはtrueとfalseのみを取ります。
文字と文字列
byteはuint8のエイリアス、runeはint32のエイリアスです。エイリアスなのでそれぞれuint8、int32と同一型です。runeはUnicodeコードポイントを表すときに使います。
ルーンリテラルはシングルクォートで囲み、1つのUnicodeコードポイントを表します。
r := 'あ'
newline := '\n'stringはイミュータブルなバイト列です。
文字列リテラルはダブルクォートまたはバッククォートで囲みます。バッククォート文字列は改行を含む生の文字列です。
s := "hello"
raw := `line1
line2`文字列はrangeでルーン単位に走査できます。
for i, r := range "Go言語" {
fmt.Printf("%d: %c\n", i, r)
}
// 0: G
// 1: o
// 2: 言
// 5: 語 // 日本語はUTF-8で1文字が複数バイトのためインデックスが飛ぶエイリアスなので、%Tはuint8とint32を表示します。
s := "Go言語"
b := []byte(s)
fmt.Println(b) // [71 111 232 168 128 232 170 158]
fmt.Println(string(b)) // Go言語
var ch byte = 'A'
var r rune = '語'
fmt.Printf("%T %c\n", ch, ch) // uint8 A
fmt.Printf("%T %c\n", r, r) // int32 語[]byte(s)とstring(b)でバイト列と文字列を相互変換できます。文字列はイミュータブルなので要素を書き換えられません。[]byteは要素を書き換えられます。変換で得たスライスを書き換えても、変換元の文字列は変わりません。[]byteから得た文字列も、その後スライスを書き換えても変わりません。
s := "hello"
b := []byte(s)
b[0] = 'J'
fmt.Println(s) // hello
fmt.Println(string(b)) // Jello
b2 := []byte{'h', 'i'}
t := string(b2)
b2[0] = 'H'
fmt.Println(t) // hi
fmt.Println(string(b2)) // Hi型定義と型エイリアス
typeで新しい名前を付けられます。
type Celsius float64 // 型定義: Celsiusはfloat64とは別の型
type ID = int // 型エイリアス: IDとintは同一型型定義は新しい型を導入し、基底型が同じでも代入には変換が必要な場合があります。型エイリアスは既存の型に別名を付けるだけです。
型変換
型付きの値どうしに暗黙変換はありません。異なる型の値を使うときはfloat64(n)のように明示的に変換します。非型付き定数は、代入先の型で表現できるなら変換なしで使えます。
var i int = 42
f := float64(i)
type Celsius float64
var c Celsius = 100 // OK: 非型付き定数100はCelsiusとして使える
var t float64 = float64(c)
// var x float64 = i // NG: 型付きのintはfloat64へ代入できないポインタ型
ポインタ型*Tは、型Tの値が格納されているメモリ上の位置であるアドレスを保持する型です。例えばint型のポインタ型は*int、配列[3]intなら*[3]intと表記します。元の型Tとポインタ型*Tは別の型であり、int型の変数に*intの値を直接代入することはできません。
未初期化のポインタのゼロ値はnilです。nilは有効なアドレスを指していない状態を表し、この状態で間接参照を行うと実行時パニックが発生します。また、Go言語にはポインタに整数を加減算するようなポインタ演算(p + 1など)はありません。
var p *int // ゼロ値はnil
fmt.Println(*p) // panic: nilポインタの間接参照アドレス取得と間接参照
&xは変数などのアドレスを取得し、型*Tのポインタを生成します。*pは間接参照(デリファレンス)であり、ポインタpが指すアドレスの実体を表します。*pを評価すると参照先の値を取得でき、*pへ代入するとその参照先の値を直接書き換えられます。
x := 42
p := &x
fmt.Println(*p) // 42
*p = 100
fmt.Println(x) // 100ポインタ変数を代入するとアドレス値がコピーされ、双方が保持する参照先が同じになります。
x := 42
p := &x
q := p
*q = 7
fmt.Println(x, *p, *q) // 7 7 7ポインタ同士の比較(==)は、同じ変数を指しているか、または両方がnilの場合にtrueになります。参照先の値が等しくても、別の変数を指していればfalseです。
a, b := 1, 1
p, q := &a, &a
r := &b
fmt.Println(p == q) // true(同じ変数a)
fmt.Println(p == r) // false(値は同じでも別の変数)サイズが0の型(空の構造体struct{}や長さ0の配列など)では、別々の変数でも同じアドレスになることがあり、その場合の==の結果は仕様上どちらでもよいとされています。
var emptyA, emptyB struct{}
pa, pb := &emptyA, &emptyB
fmt.Println(pa == pb) // 結果は仕様で決まっていないアドレス可能な式
&xでアドレスを取得できるのは、xがアドレス可能な式(addressable)の場合に限られます。変数、間接参照(*p)、スライスの要素、アドレス可能な構造体のフィールド、アドレス可能な配列の要素が該当します。配列・スライス・マップ・構造体の複合リテラルも例外としてアドレスを取得できます。
一方、数値や文字列などの基本リテラル、定数(const)、マップの要素、関数の戻り値などはアドレスを取得できません。
n := 10
p := &n // OK: 変数
q := &(*p) // OK: 間接参照(pは非nil。同じ変数nを指す)
nums := []int{1, 2}
r := &nums[0] // OK: スライスの要素
arrLit := &[3]int{1, 2, 3} // OK: 配列の複合リテラル
sliceLit := &[]int{1, 2} // OK: スライスの複合リテラル
mapLit := &map[string]int{"a": 1} // OK: マップの複合リテラル
// &10 // NG: 基本リテラルはアドレスを取れない
const c = 10
// &c // NG: 定数はアドレスを取れない
m := map[string]int{"a": 1}
// &m["a"] // NG: マップの要素はアドレスを取れない
// &time.Now() // NG: 関数の戻り値はアドレスを取れない
// var np *int
// _ = &(*np) // NG: nilの間接参照はパニック値渡しとポインタ
Go言語の関数呼び出しは常に値渡しです。引数に変数を渡すとその値のコピーが渡るため、関数内で引数を書き換えても呼び出し元の変数は変化しません。
呼び出し元の変数を直接書き換えたい場合は、ポインタを渡します。ポインタを渡す場合もアドレス値自体はコピーされますが、参照先の実体が共有されるため、間接参照(*n)を経由して呼び出し元の値を更新できます。
func setToZero(n int) { n = 0 } // コピーされた引数nのみが0になる
func setToZeroPtr(n *int) { *n = 0 } // *n経由で呼び出し元の変数を0にする
count := 5
setToZero(count)
fmt.Println(count) // 5
setToZeroPtr(&count)
fmt.Println(count) // 0newによる割り当て
組み込み関数newは引数として型または式を1つ受け取り、初期化した変数へのポインタを返します。new(x)を使うと、中間変数を置かずに値xへのポインタを1つの式で得られます。
型Tを渡したnew(T)は、型Tをゼロ値で初期化した変数を確保し、そのポインタ*Tを返します。式xを渡したnew(x)は、式xの型の変数をxの値で初期化して確保し、そのポインタを返します。非型付き定数を渡した場合はそのデフォルト型(new(42)なら*int)になります。事前宣言された識別子nilはnewの引数にできません。
// 型を渡すとゼロ値へのポインタを返す
pZero := new(int)
fmt.Println(*pZero) // 0
// 式を渡すとその値で初期化されたポインタを返す
pVal := new(42)
fmt.Println(*pVal) // 42
// new(nil) // NG: nilはnewの引数にできないスライス、マップ、チャネルに対してnewを使うと、ゼロ値(nil)を指すポインタが返ります。スライスのゼロ値はappendできますが、戻り値を代入し直す必要があります。マップのゼロ値への要素代入はパニックになります。チャネルのゼロ値への送受信は、そのゴルーチンでは完了せず永久に待ち続けます。nilチャネルをcloseするとパニックになります。
送受信できるチャネルと、指定した長さや容量を持つスライスを作るには、内部データ構造を初期化した値そのものを返すmakeを使います。makeの戻り値はポインタではありません。
pMap := new(map[string]int) // *map[string]int(*pMapはnil)
// (*pMap)["key"] = 1 // NG: nilマップへの代入でパニック
m := map[string]int{} // リテラルで初期化したら書き込み可能
m["key"] = 1
pSlice := new([]int) // *[]int(*pSliceはnil)
*pSlice = append(*pSlice, 1) // OK: 戻り値を代入すればappendできる
s := make([]int, 0, 5) // []int(指定したlenとcap)
ch := make(chan int) // chan int(送受信できる)構造体型
構造体は、名前と型を持つフィールドを並べた型です。フィールドは宣言した順に並び、名前は重複できません。同じ型のフィールドはX, Y float64のように1行にまとめられます。フィールド名も識別子なので、先頭が大文字なら他パッケージから参照できます。フィールドにアクセスするときは、値.フィールド名の形式で書きます。
構造体型の変数を定義して何も代入しない場合、各フィールドはゼロ値で埋まります。
type Point struct {
X, Y float64
}
var p Point
p.X = 1
fmt.Println(p.X) // 1_はブランク識別子で、フィールド名にも使えますが、名前として参照できないためp._とは書けません。同じ構造体に何度でも書くことができ、領域だけ確保するパディングの用途で使用できます。
type Header struct {
Flag uint8
_ [3]byte
Size uint32
}構造体リテラル
フィールドをまとめて初期化するときは構造体リテラルを使います。
p1 := Point{1, 2} // Pointの値
p2 := Point{X: 1, Y: 2} // Pointの値
p3 := &Point{X: 1, Y: 2} // *Point
// p3と同じ意味
p := Point{X: 1, Y: 2}
p4 := &p // *Pointp1とp2は構造体型の値です。p3は構造体リテラルのアドレスをその場で取った*Pointです。pを用意してp4 := &pとする書き方も同じ意味で、p3とp4は同じ型・同じ内容のポインタになります。
*Pointを引数に取る関数へ渡すときや、構造体のフィールドがポインタ型のときに&T{...}がよく使われます。ゼロ値へのポインタだけ欲しいときはnew(Point)も使えますが、フィールドを初期値で埋めたいときは&Point{X: 1, Y: 2}の方が意図が読み取りやすいです。
埋め込みによる合成
フィールドに別の型名を書くと埋め込みになります。埋め込みは継承ではなく合成です。埋め込まれたフィールドやメソッドは外側に昇格し、c.Powerのように直接アクセスできます。
type Engine struct{ Power int }
type Car struct {
Engine
Model string
}
c := Car{Engine: Engine{Power: 150}, Model: "compact"}
fmt.Println(c.Power) // 昇格比較
すべてのフィールドの型が比較可能なときだけ==と!=が使えます。スライス、マップ、関数を含む構造体は比較できません。等価なのは、型が同じで、対応する_以外のフィールドがすべて等しいときです。
a := Point{X: 1, Y: 2}
b := Point{X: 1, Y: 2}
fmt.Println(a == b) // true
c := Point{X: 1, Y: 3}
fmt.Println(a == c) // false
type Vertex struct {
X, Y float64
}
// fmt.Println(a == Vertex{X: 1, Y: 2}) // コンパイルエラー: 型が違う
fmt.Println(a == Point(Vertex{X: 1, Y: 2})) // true
type Record struct {
Tags []string
}
var r1, r2 Record
// fmt.Println(r1 == r2) // コンパイルエラー: 比較できないフィールドがある配列
配列[n]Tは、型Tの要素を固定数n個並べたデータ構造です。要素数nは型情報の一部であり、[3]intと[4]intは異なる型として扱われます。型が異なるため、相互に代入することはできません。
var a [3]int
// var b [4]int = a // コンパイルエラー: 型が異なるGo言語の配列は値型です。配列を変数に代入したり関数へ引数として渡したりすると、すべての要素が丸ごとコピーされます。
arr := [3]int{1, 2, 3}
dup := arr // 配列全体のコピー
dup[0] = 0
fmt.Println(arr[0], dup[0]) // 1, 0(元の配列arrは変わらない)配列の要素数が多い場合、丸ごとのコピーはオーバーヘッドになります。また、長さが型に固定されているため、要素数が可変のデータを扱う用途には適していません。そのため、Go言語では可変長データを扱うコレクションとして主にスライス[]Tを用います。
スライス
スライス([]T)は、配列の連続した領域を参照する可変長のデータ構造です。
スライス値の実体は、元配列へのポインタ、参照している要素数(len)、参照可能な最大容量(cap)の3つのフィールドからなるヘッダです。capは再割り当てなしに要素を追加できる上限を表します。
スライスを変数へ代入したり関数へ渡したりした際にコピーされるのは、この小さなヘッダのみです。元配列の実体は共有されるため、一方のスライスを経由して要素を書き換えると、同じ元配列を参照している他のスライスにも変更が反映されます。
スライスはリテラルや組み込み関数makeで作成できます。
nums := []int{1, 2, 3}
buf := make([]byte, 0, 64) // len=0, cap=64s[low:high]というスライス式は、元配列の一部を参照する新しいスライスを生成します。スライス自体は別個に作られますが、要素が格納されている元配列は共有されます。例えばall[low:high]と書くと、インデックスlow以上high未満の要素を切り出します。
all := []int{0, 1, 2, 3, 4}
mid := all[1:4] // midは[1, 2, 3]、len=3
mid[0] = 99
fmt.Println(all) // [0 99 2 3 4](元配列が書き換わるためall側にも反映される)s[low:high]で生成されたスライスの容量はcap(元) - lowです。元のallのcapが5、lowが1の場合、切り出されたmidのcapは4になります。
fmt.Println(len(mid), cap(mid)) // 3, 4s[low:high:max]というスライス式を使うと、スライスの容量をmax - lowに明示的に制限できます。部分スライスが元配列の空き容量を意図せず上書きするのを防ぎたい場合に有効です。
fullCap := all[1:4:4] // len=3, cap=3(max - low = 4 - 1)
fmt.Println(len(fullCap), cap(fullCap)) // 3, 3スライスに対してappendを実行した際の挙動は、そのスライスのcapに空きがあるかによって異なります。capに空きがある場合、新しい配列は確保されず、共有している元配列へ直接値が書き込まれます。元配列の要素に既に値がある場合は、その要素の値が上書きされます。一方、capに空きがない場合、別のメモリ領域に新しい配列を丸ごと確保し、既存の要素をコピーした上で末尾に追記します。部分スライスの参照先はこの新しい配列を指すように切り替わるため、元のスライスや元配列の内容は書き換えられず、参照関係が切り離されます。
all := []int{0, 1, 2, 3, 4}
wide := all[1:4] // len=3, cap=4(元配列のインデックス4に既に値がある)
wide = append(wide, 9)
fmt.Println(all) // [0 1 2 3 9](元配列のall[4]が9で上書きされる)
all = []int{0, 1, 2, 3, 4}
tight := all[1:4:4] // len=3, cap=3(空き容量なし)
tight = append(tight, 9) // 新しい配列が別個に確保されてコピーされ、tightの参照先が切り替わる
fmt.Println(all) // [0 1 2 3 4](元のallや元配列は影響を受けない)スライス間の要素共有を切り離したい場合は、makeで独立した元配列を確保し、組み込み関数copyで要素を複製します。
all := []int{0, 1, 2, 3, 4}
mid := all[1:4] // [1, 2, 3]
dst := make([]int, len(mid)) // 独立した新しい元配列を確保
copy(dst, mid) // 要素をコピー
dst[0] = 0
fmt.Println(dst[0], mid[0]) // 0, 1(dstの変更はmidやallに影響しない)未初期化のnilスライスであっても、lenやcapの取得、appendによる要素追加は正常に動作します。
var s []int
fmt.Println(s == nil, len(s), cap(s)) // true 0 0
s = append(s, 10, 20)
fmt.Println(s, s == nil) // [10 20] falseappendは要素を追加した新しいスライスを返すため、戻り値を変数へ代入し直さないと長さ(len)の変更が反映されません。
nums := []int{1, 2, 3}
nums = append(nums, 4, 5) // [1, 2, 3, 4, 5]
s := []int{1}
append(s, 2) // 戻り値を捨てるとsのlenは1のまま
s = append(s, 2) // 再代入することでlen=2になる
v := []int{1}
t := v
t = append(t, 99) // 変数tのみlen=2に更新
fmt.Println(len(v), len(t)) // 1, 2(vは変化しない)関数へ引数としてスライスを渡す場合も、値渡しによりスライスの情報がコピーされます。関数内でappendを実行しても、呼び出し元のスライス変数のlenは変化しません。ただし、capに空きがあって元配列の再割り当てが発生しなかった場合は、元配列の内容自体が関数内から書き換えられることがあります。
func grow(s []int) {
s = append(s, 99) // 関数内の変数sのみlen=2になる
}
data := make([]int, 1, 2) // len=1, cap=2
grow(data)
fmt.Println(len(data)) // 1(呼び出し元のスライスのlenは不変)
fmt.Println(data[1]) // 99(capに空きがあったため元配列には書き込まれている)
narrow := []int{1} // len=1, cap=1
grow(narrow)
fmt.Println(len(narrow), narrow) // 1, [1](cap不足で別配列に再割り当てされたため影響なし)スライスを入れ子にすることで、多次元データ構造を表現できます。
rows, cols := 2, 3
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}スライスをfor-rangeで走査する際、ループ内で要素を追加してスライス自体の長さを変更すると、意図しない無限ループや要素の読み飛ばしにつながる恐れがあります。走査対象のスライス自体をループ内で変更することは避けてください。
// 避けるべき例: 走査中にスライスを変更する
s := []int{1, 2, 3}
for i, v := range s {
s = append(s, v) // 反復対象や終了条件が不定になる
}マップ
マップ(map[K]V)は、キーと値を結びつけるハッシュテーブルです。
キー型Kには、比較演算子==および!=が定義されている型(comparableな型)のみ指定できます。スライス、マップ、関数型は比較できないためキーに指定できません。構造体は、すべてのフィールドの型が比較可能である場合に限りキーとして使用できます。
makeまたはリテラルを用いて初期化し、キーを指定して要素の読み書きを行います。
scores := make(map[string]int) // map[string]int{"go": 90} でも初期化可能
scores["go"] = 90
scores["rust"] = 85キーの存在確認には2値受け取りを用います。指定したキーが存在しない場合、2番目の戻り値okがfalseとなり、1番目の戻り値には値型のゼロ値が返ります。
fmt.Println(scores["go"]) // 90
v, ok := scores["java"]
fmt.Println(v, ok) // 0, false(キーが存在しない)不要になったキーは組み込み関数deleteで削除します。
delete(scores, "go")
fmt.Println(scores["go"], scores["rust"]) // 0, 85未初期化のマップ変数はnilです。nilマップからの読み取りやdeleteの実行はパニックになりませんが、要素を書き込もうとすると実行時パニックが発生します。
var m map[string]int
// m["k"] = 1 // panic: nilマップへの書き込み
v, ok := m["k"] // v=0, ok=false(読み取りは安全)
delete(m, "k") // キーが存在しなくても安全マップをfor-rangeで走査する際の順序は、言語仕様として保証されていません。実装上も走査ごとに順序がランダム化され、プログラムが特定の反復順序に依存しないよう設計されています。
scores := map[string]int{"go": 90, "rust": 85, "java": 80}
for lang, score := range scores {
fmt.Println(lang, score)
}
// 走査を実行するたびに出力順が変わることがあるキーの昇順など一定の順序で処理したい場合は、一度キーをスライスに集めてソートしてから参照します。
keys := make([]string, 0, len(scores))
for lang := range scores {
keys = append(keys, lang)
}
sort.Strings(keys)
for _, lang := range keys {
fmt.Println(lang, scores[lang]) // go, java, rustの昇順で出力
}型定義
type T Uと宣言することで、型Uと同じ性質を持つ別の型Tを定義することができます。例えば、type Celsius float64と定義したとき、Celsiusとfloat64は別の型ですが、明示的な変換をすることで同じ値を代入することができます。
var c Celsius = 36.5
var f float64 = float64(c) // OK
// var f float64 = c // コンパイルエラー型エイリアス
type T = Uで定義できる型エイリアスでは、型Tと型Uは同一型として扱うことができます。
type Count = int
var n int = 0
var c Count = n // 同一型なので明示的な型変換が不要基底型
各型には基底型があります。基底型とは、type T Uによる定義をたどったとき終端にある型のことです。
type ID int
type UserID ID // UserIDの基底型はint複合型の同一性
配列・スライス・構造体・ポインタ・関数・インターフェース・マップ・チャネルといった複合型では、種類と内部の型構成が一致しなければ同一型とはみなされません。配列では要素型に加えて長さも型の一部になり、構造体ではフィールドの名前・型・並びが全て一致している必要があります。
[]intと[]intは同一型[]intと[3]intは別の型type A intとtype B intは別の型struct{ x int }とstruct{ y int }は別の型Stack[T any]や[]Tのように型パラメータTを含む型は、具体的な型引数を与えてインスタンス化したあとに同一型かどうか決まる。例えば、型引数を与えた結果Stack[int]とStack[string]となれば別の型になります。
代入可能性
値vを型Tの変数に代入できるかは、型の関係をもとにコンパイル時に判定されます。
同一型どうしはそのまま代入可能です。
var a int = 1
var b int = a基底型が同一の型定義どうしは、明示的に変換することで代入可能です。
var c Celsius = 36.5
var f float64 = float64(c)nilはポインタ、スライス、マップ、チャネル、関数、インターフェースに代入可能です。
var ptr *int = nil
var slice []int = nil
var m map[string]int = nil
var ch chan int = nil
var fn func() = nil
var i any = nilインターフェース変数には、そのインターフェースを実装する非インターフェース型の値を代入可能です。
import (
"errors"
)
// 組み込みのerrorインターフェースを実装した非インターフェース型が返る
var err error = errors.New("something went wrong")表現可能性
定数の型変換と代入において、値が型Tの取りうる値の範囲に収まるかはコンパイル時に判定されます。
const limit = 300
var wide int = limit // OK: 300はintの範囲内
// var tiny int8 = limit // コンパイルエラー: 300はint8の範囲外型を持たない定数は、その値が代入先の型で正確に表現できる場合のみ変換が可能です。
const whole = 1.0
var n int = int(whole) // OK
const frac = 1.5
// var n int = int(frac) // NG: 1.5は整数として正確に表現できない
const letter = 'A'
var b byte = letter // OK: 'A'は65でbyteに表現できる
var one string = string(letter) // OK: runeを1文字のstringに変換
const text = "go"
var s string = text // OK
var bs []byte = []byte(text) // OK: stringをバイト列に変換
var first byte = text[0] // OK: 先頭バイト 'g' (103)
// var lone byte = byte(text) // NG: 文字列全体はbyte1個に表現できない型付き定数は宣言時点でその型に表現できる値だけが許されます。
const max int8 = 127 // OK
// const bad int8 = 300 // NG: 300はint8に表現できない変数の型変換でコンパイルが通るかは、型の互換性によって決まります。値が範囲がだったとしても、float64からintのようにどちらも数値型の場合はコンパイルエラーは発生しません。実行時に切り捨てやオーバーフローが発生します。stringからintのような変換は互換性がないためコンパイルエラーになります。
// 定数の場合はコンパイルエラー
// const x float64 = 1.5
// var y int = int(x)
// 変数の場合はコンパイル可能
var n float64 = 1.5
var m int = int(n) // 切り捨てられて1になるインターフェース型
インターフェース型は、具体的な型を指定せず呼び出せるメソッドの条件だけを書いた型です。条件を満たす値なら、中身の型が違っても同じ変数に入れられます。
type Shape interface {
Area() float64
}メソッドを1つも宣言しないインターフェースは空インターフェースと呼ばれ、任意の型(インターフェース型を含む)を格納できます。anyは空インターフェースの別名です。
インターフェース型の変数の中身
インターフェース型の変数には、コンパイル時に決まる静的型と、実行時に決まる動的型・動的値が入ります。
静的型(static type)
コンパイル時に確定する型のことです。var a Shapeと定義されていればShapeが静的型です。実行中にaの中身が*Rectでも別の型でも、静的型はShapeのままです。
動的型(dynamic type)
実行時にそのインターフェースの中に実際に格納されている具体的な型のことです。aに*Rectを入れれば動的型は*Rectとなり、別の値を入れ直せば動的型も変わります。何も代入されていない初期状態では動的型も nil です。
動的値(dynamic value)
実行時にそのインターフェースの中に実際に格納されているデータのことです。*Rectを入れていれば、データが格納されているアドレスへのポインタ(またはnil)が動的値です。
暗黙の実装
オブジェクト指向な言語とは異なり、implementsのような宣言はありません。型がインターフェースが要求するメソッドをすべて備えていれば、宣言なくそのインターフェースを実装したとみなされます。
type Shape interface {
Area() float64
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}
var a Shape = Rect{W: 3, H: 4} // RectがShapeのメソッドを持つので代入可能値型Tのメソッド集合にはレシーバがTのメソッドが入ります。ポインタ型*Tのメソッド集合には、レシーバがTまたは*Tのメソッドが入ります。次の例では、値型Rectのメソッド集合にはAreaだけが含まれ、ポインタ型*Rectのメソッド集合にはAreaとScaleが含まれます。
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r *Rect) Scale(f float64) { r.W *= f; r.H *= f }メソッド集合が異なるため、要求するメソッドによって代入可否が変わります。
type Shape interface { Area() float64 }
type Scaler interface { Scale(float64) }
var a Shape = Rect{} // OK: RectにAreaがある
// var s Scaler = Rect{} // NG: RectにScaleがない
var s Scaler = &Rect{} // OK: *RectにScaleがある埋め込みによって埋め込まれた型のメソッドは外側の型のメソッド集合に入ります。
type Shape interface { Area() float64 }
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
type Frame struct {
Rect
}
var a Shape = Frame{} // OK: AreaがFrameのメソッド集合に入るnilの扱い
インターフェース型の変数では、動的型と動的値の両方がnilのときだけnilと等価になります。
インターフェース型の変数のゼロ値は、動的型も動的値もnilです。
var a Shape
fmt.Println(a == nil) // truenilポインタをインターフェースに入れると、ポインタ自体はnilでもインターフェースとしては空ではないためnilと等価になりません。
var p *Rect
fmt.Println(p == nil) // true: ポインタ変数としてはnil
var a2 Shape = p
fmt.Println(a2 == nil) // false: 動的型*Rectがあるインターフェースの埋め込み
インターフェース定義に別のインターフェース型名を並べると埋め込みになります。外側のインターフェースは、内側のインターフェースが要求するメソッドをすべて引き継ぎます。
type Shape interface {
Area() float64
}
type Perimer interface {
Perimeter() float64
}
type Measurer interface {
Shape
Perimer
}
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r Rect) Perimeter() float64 { return 2 * (r.W + r.H) }
type Circle struct{ R float64 }
func (c Circle) Area() float64 { return 3.14159 * c.R * c.R }
var m Measurer = Rect{} // OK
var a Shape = Rect{} // OK
var p Perimer = Rect{} // OK
// var c Measurer = Circle{} // NG: Perimeterがない型アサーション
インターフェース値から具体的な値を取り出すには型アサーションを使います。s, ok := v.(T)の2値形式で取り出し、動的型がTでなければokがfalseになります。
var v any = Rect{W: 3, H: 4}
r, ok := v.(Rect)
if !ok {
// 動的型がRectでない
return
}
// ここではrをRectとして使えるokを受け取らないs := v.(T)は、動的型がTでないときにパニックになります。ユーザー入力や外部データから復元するときは、必ず2値形式で分岐します。
複数の動的型を分岐するときはswitch v := x.(type)の型スイッチが使えます。動的型に応じてcaseが選ばれ、どのcaseにも当てはまらなくてもパニックになりません。
func show(x any) {
switch v := x.(type) {
case Rect:
fmt.Println("rect:", v)
case Circle:
fmt.Println("circle:", v)
default:
fmt.Println("other type")
}
}インターフェース実装の確認
コンパイラは代入が起きたときにメソッド集合を見てインターフェースを満たすか判断します。型定義の近くにインターフェース型の変数への代入を書いておくと、メソッドの追加漏れやレシーバ型の取り違えをビルド時に検知できます。
type Shape interface { Area() float64 }
type Scaler interface { Scale(float64) }
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}
func (r *Rect) Scale(f float64) {
r.W *= f
r.H *= f
}
var _ Shape = Rect{} // OK
// var _ Scaler = Rect{} // NG: RectにはScaleがない
var _ Scaler = (*Rect)(nil) // OK左辺の_は値を使わない時に用いる識別子です。(*Rect)(nil)は実体を作らず、右辺の型が*Rectであることだけをコンパイラに伝えます。
型パラメータと制約
関数や型の名前の直後に角括弧で型パラメータと制約を宣言することで、具体的な型に依存しない汎用的なコードを記述できます。たとえば[T any]と書くと、Tが型パラメータ、anyが制約を表します。型パラメータは呼び出し時やインスタンス化時にintなどの具体的な型に置き換わります。
制約は、型引数として受け入れ可能な型の集合を定めます。型パラメータに対して実行できる演算や呼び出せるメソッドも制約によって決まります。
func Min[T any](a, b T) T {
if a < b { // コンパイルエラー: Tがanyでは<が使えない
return a
}
return b
}順序比較演算子<や>を使うには、順序付けが可能な型を表す制約が必要です。標準ライブラリのcmpパッケージが提供するcmp.Orderedを指定します。
import "cmp"
func Min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}ジェネリックな関数を使用する際は、型パラメータに具体的な型引数を渡してインスタンス化します。関数の呼び出しでは引数の型から型引数が推論されるため、通常は型引数の指定を省略できます。
n := Min(3, 5) // 引数からTがintと推論される
m := Min[int](3, 5) // 角括弧で型引数を明示的に指定型引数はコンパイル時に確定します。インターフェースのように実行時に動的型が変化するのではなく、静的な型として扱われます。
制約の定義
型制約はインターフェースとして定義します。
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}|は型集合の和集合を表します。チルダ記号~は基底型を指定し、~intと書くことで基底型がintであるすべての型を集合に含めます。int自身に加え、type MyInt intのように新しく定義した型もNumberを満たすため、Sum([]MyInt{1, 2, 3})のように呼び出せます。
any
組み込みのanyは、すべての型を許容する制約です。
comparable
組み込みのcomparableは、等値比較演算子==および!=が使用できる型に限定する制約です。マップのキーとして使う型パラメータなどに指定します。制約としてのみ使用でき、変数やフィールドの型としては宣言できません。
ジェネリック型
構造体などの型定義で型パラメータを宣言できます。定義した型パラメータは、フィールドやメソッドレシーバで使用します。
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
var s Stack[int]
s.Push(10)
// s.Push("text") // コンパイルエラー: stringはintに代入できないStack[int]のように型引数を指定すると、フィールドの[]Tやメソッド引数のTがすべてintに置き換わります。
ジェネリックメソッド
Go言語のバージョン1.27以降では、メソッド名の直後にも独自の型パラメータを宣言できます。レシーバの型パラメータとは独立した型パラメータを追加できるため、別の型へ変換する処理などをパッケージレベルの関数に切り出すことなくその型のメソッドとして定義できます。
func (s Stack[T]) Map[U any](f func(T) U) Stack[U] {
items := make([]U, len(s.items))
for i, v := range s.items {
items[i] = f(v)
}
return Stack[U]{items: items}
}渡す関数の引数と戻り値から型引数Uが推論されます。
var numbers Stack[int]
numbers.Push(1)
numbers.Push(2)
// intのStackからboolのStackへ変換
flags := numbers.Map(func(v int) bool {
return v > 0
})if
ifは条件式がtrueのとき、続くブロックを実行します。else ifやelseで条件分岐を継続できます。
if x < 0 {
return -x
} else if x == 0 {
return 0
}
return x条件式の前にセミコロンで区切った初期化文を記述できます。初期化文で宣言した変数のスコープは、続くelse ifやelseを含むif文全体です。ブロックの外からは参照できません。
if n, err := strconv.Atoi(s); err != nil {
return err
} else {
fmt.Println(n) // nはここまで有効
}
// fmt.Println(n) // コンパイルエラーfor
繰り返し処理はfor文で記述します。条件式のみ、初期化と後処理を含む3文形式、range節の3通りの書き方があります。
条件式
条件式を指定すると、評価結果がtrueの間ブロックを繰り返します。条件式を省略した場合は無限ループとなり、breakやreturnで終了します。
for n < 100 {
n *= 2
}for {
if done {
break
}
}初期化・条件・後処理
初期化文、条件式、後処理をセミコロンで区切って並べます。
for i := 0; i < 10; i++ {
fmt.Println(i)
}Go言語1.22以降、初期化文で宣言したループ変数は反復ごとに新しく作られます。ループ内で定義した関数から参照しても変数は共有されず、各反復の値がそのまま保持されます。
var prints []func()
for i := 0; i < 3; i++ {
prints = append(prints, func() { fmt.Println(i) })
}
for _, p := range prints {
p() // 0 1 2
}range
配列、配列へのポインタ、スライス、文字列、マップ、チャネル、整数、イテレータ関数を走査します。rangeで宣言したループ変数は、Go言語1.22以降は反復ごとに新しく作られます。
配列、配列へのポインタ、スライス、文字列では添字と値、マップではキーと値を受け取れます。マップの走査順序は決まっていません。チャネルでは受信した値の1つのみを受け取ります。
// スライスは添字と値を受け取る
s := []string{"a", "b"}
for i, v := range s {
fmt.Println(i, v)
}
// マップはキーと値を受け取る。走査順序は決まっていない
m := map[string]int{"x": 1, "y": 2}
for k, v := range m {
fmt.Println(k, v)
}
// チャネルは受信した値のみを受け取る
ch := make(chan int, 2)
ch <- 10
ch <- 20
close(ch)
for v := range ch {
fmt.Println(v) // 10 20
}Go言語1.22以降では、指定した回数だけ繰り返す用途として整数nも走査できます。for i := range nと記述すると0からn-1まで昇順に取り出します。nが0以下の場合は反復しません。
for i := range 3 {
fmt.Println(i) // 0 1 2
}Go言語1.23以降では、関数をrangeで走査できます。イテレータ関数の中でyieldを呼び出すたびに、走査する側のforループ本体が1回実行されます。yieldは通常trueを返しますが、走査側のループがbreakなどで終了するとfalseを返します。この戻り値を判定することで、呼び出し側でループが終了したことを検知し、不要な処理が続くのを防ぐことができます。イテレータ関数がリターンすると走査全体が終了します。
// 呼び出し側に値を渡すイテレータ関数
seq := func(yield func(int) bool) {
for n := 0; n < 3; n++ {
if !yield(n) {
return // 呼び出し側のループ終了を検知し、無駄な処理の継続を防ぐ
}
}
}
// イテレータ関数をrangeで走査
for n := range seq {
fmt.Println(n) // 0 1 2
}イテレータ関数のシグネチャには、値を渡さない形式、1つの値を渡す形式、2つの値を渡す形式の3パターンがあります。
// 値なし
noVal := func(yield func() bool) {
if !yield() {
return
}
}
for range noVal {
// 反復のみ行う
}
// 1値
singleVal := func(yield func(string) bool) {
if !yield("hello") {
return
}
}
for v := range singleVal {
fmt.Println(v) // hello
}
// 2値
pairVal := func(yield func(string, int) bool) {
if !yield("a", 1) {
return
}
}
for k, v := range pairVal {
fmt.Println(k, v) // a 1
}breakとcontinue
breakは最も内側のfor、switch、selectを終了し、continueは最も内側のforの次の反復へ進みます。入れ子のループで外側を終了したり外側の反復を進めたりするには、対象のfor文にラベルを付けてbreak ラベル名やcontinue ラベル名と指定します。
Outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if j == 1 {
continue // 内側ループの次の反復へ進む
}
if j == 2 {
break // 内側ループだけを終了して外側の次のiへ進む
}
if i == 1 {
break Outer // 外側ループごと終了する
}
fmt.Println(i, j)
}
}
// 0 0 を出力して終了switch
switchは式の値に一致したcaseの本体を実行します。一致するcaseがなくdefault節があるときは、その処理を実行します。default節を省略し、どのcaseにも当てはまらないときは何もしません。1つのcaseにカンマ区切りで複数の値を並べると、いずれかに一致したときに実行されます。
switch day {
case "Mon":
fmt.Println("月曜")
case "Tue", "Wed":
fmt.Println("火〜水")
default:
fmt.Println("その他")
}各節の処理が終わると自動的にswitch文を抜けます。後続の節へ処理を継続したい場合のみ、節の末尾にfallthroughを記述します。fallthroughは次のcaseの条件判定を行わずに直後の本体を実行します。最後の節には記述できません。
switch rank {
case 1:
fmt.Println("金")
fallthrough
case 2:
fmt.Println("銀")
case 3:
fmt.Println("銅")
}
// rankが1のとき: 金と銀を出力
// rankが2のとき: 銀を出力
// rankが3のとき: 銅を出力判定式を省略したswitchは、各caseの条件式を上から順に評価し、最初にtrueとなった節を実行します。
switch {
case x < 0:
fmt.Println("負")
case x == 0:
fmt.Println("ゼロ")
default:
fmt.Println("正")
}式の前にセミコロンで区切った初期化文を記述できます。宣言した変数のスコープはswitch文全体です。
switch n, err := strconv.Atoi(input); {
case err != nil:
fmt.Println("invalid")
case n == 0:
fmt.Println("zero")
default:
fmt.Println(n) // nはswitch全体で有効
}
// fmt.Println(n) // コンパイルエラー型スイッチ
インターフェース値の動的型で分岐します。switch v := x.(type)で導入した変数vは、節ごとに暗黙のブロック内で別々に宣言されます。単一の型を指定した節ではvはその型として扱われ、複数の型を並べた節やdefault節では元の値xと同じ型になります。型スイッチではfallthroughを使用できません。
func describe(x any) {
switch v := x.(type) {
case int:
fmt.Println("int", v) // vはint型
case string:
fmt.Println("string", v) // vはstring型
default:
fmt.Println("other", v) // vは元のxと同じany型
}
}定義と戻り値
関数はfuncキーワードで定義します。名前、引数、戻り値の型を書き、returnで値を返します。続く引数が同じ型のとき、型を1回だけ書いてa, b intと省略できます。
func add(a, b int) int {
return a + b
}
func mix(a, b int, factor float64) float64 {
return float64(a+b) * factor
}複数戻り値とerror
戻り値の型を(T1, T2)のように並べると、複数の値を返せます。代入で受け取るときは、左辺の個数を戻り値の個数に合わせます。
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
v, err := divide(10, 2)
if err != nil {
return err
}defer
deferは、それを書いた関数が呼び出し元へ戻る直前に実行する処理を登録する文です。return文、関数の末尾への到達、そのゴルーチンのpanicのいずれでも実行されます。関数スコープで後片付けを書くために使われます。
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()複数のdeferは後入れ先出しで実行されます。登録時に関数値と引数が評価され、実際の呼び出しは関数終了まで遅延します。ループ内でdeferを書くと、登録は繰り返されますが実行は関数終了まで遅延します。
for i := 0; i < 3; i++ {
defer fmt.Println(i) // 登録時にiが評価される → 終了時に2, 1, 0の順
}名前付き戻り値
戻り値の型の前に名前を付けると、その名前が関数内の変数として使えます。関数に入った時点で、各戻り値は型のゼロ値です。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // この時点のx, yを返す(return x, yと同じ)
}return文に値を指定した場合、まず名前付き戻り値の変数にその値が代入され、そのあとでdeferが実行されます。そのため、deferの中で名前付き戻り値を書き換えると、最終的に呼び出し元へ渡る値が変わります。
func scaled() (result int) {
defer func() {
result *= 7
}()
return 6 // 呼び出し側には42が渡る
}可変長引数
末尾の引数を...Tと宣言すると可変長引数になり、呼び出し側は0個以上の値を渡せます。関数内では型[]Tのスライスとして扱われます。引数を1つも渡さなかった場合、関数内のスライスはnil(長さ0、容量0)になります。
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum()
sum(1, 2, 3)スライスを渡すときは、末尾に...を付けます。新しいスライスは作られず、呼び出し元のスライスがそのまま...Tの引数になります。変数でもリテラルでもよく、sum(origin...)やsum([]int{4, 5}...)のように書けます。
...Tと宣言された引数に対して個別の値とスライス展開を同時に渡すこと(sum(1, s...))はできません。通常の引数のあとに可変長引数がある関数では、通常の引数とs...を並べられます。
func greet(prefix string, names ...string) {}
greet("hi:", []string{"Ann", "Bo"}...)値を個別に列挙して渡す場合と、スライスを...で渡す場合では、メモリ上の扱いに違いがあります。個別の値を列挙すると、呼び出し時に新しいスライスと、それが参照する配列が割り当てられ、引数の値がそこへコピーされます。...付きで渡すと配列は共有されるため、関数内で要素を書き換えると呼び出し元のスライスにも反映されます。
func replaceFirst(nums ...int) {
if len(nums) > 0 {
nums[0] = 0
}
}
origin := []int{10, 20, 30}
replaceFirst(origin...)
fmt.Println(origin) // [0 20 30] 元のスライスが参照している配列が書き換わる関数値
関数は第一級の値です。関数型の変数に代入して、別名から呼び出せます。
func twice(n int) int {
return n * 2
}
var fn func(int) int = twice
z := fn(3) // zは6関数リテラル
funcに名前を付けず書いた式が関数リテラルです。無名関数ともいいます。
add := func(x, y int) int {
return x + y
}
total := add(1, 2) // 3関数リテラルの直後に関数を実行する()を付けると、変数に代入せずその場で呼び出せます。
n := func(x int) int { return x * 2 }(3) // 6関数リテラルはクロージャです。外側の関数で宣言した変数を参照でき、その変数はリテラルと共有され、リテラルが参照可能な間は保持されます。次の例でクロージャなのは、counterが返す内側の関数です。この関数は外側のnを参照します。next()を呼ぶたびに同じnが更新され、1, 2と増えます。
func counter() func() int {
n := 0
return func() int { // この関数リテラルがクロージャ
n++
return n
}
}
next := counter()
v1 := next() // 1
v2 := next() // 2メソッドの定義
メソッドは型に紐づく関数です。メソッドにはレシーバという特殊な引数が必要で、どの型の値に対して実行するかを指定します。
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 {
return r.W * r.H
}型に紐づくため、オブジェクト指向な言語とは異なり、構造体以外の型にもメソッドを定義することができます。
type Length float64
func (l Length) Double() Length {
return l * 2
}同じ型のメソッド集合内でレシーバ名は一意である必要があります。
値レシーバとポインタレシーバ
レシーバには値レシーバとポインタレシーバの二つがあります。値レシーバは、呼び出し元の値のコピーを受け取って実行するため、元の値に副作用がありません。ポインタレシーバは、呼び出し元の値へのポインタを受け取ってから実行するため、元の値を変更することができます。
func (r Rect) Area() float64 {
return r.W * r.H
}
// 呼び出し元の値を書き換えている
func (r *Rect) Scale(f float64) {
r.W *= f
r.H *= f
}値レシーバで定義されたメソッドは、値(Rect型)からしか呼び出すことができません。同様にポインタレシーバで定義されたメソッドは、ポインタ(*Rect型)からしか呼び出すことができません。しかし、コンパイル時に自動補完が走るため、どちらのケースでも変数から呼び出す形で問題なく動きます。
type Rect struct{ W, H float64 }
func (r Rect) Area() float64 { return r.W * r.H }
func (r *Rect) Scale(f float64) { r.W *= f; r.H *= f }
r := Rect{W: 3, H: 4}
r.Scale(2) // コンパイラが (&r).Scale(2) に読み替える
fmt.Println(r.Area())
p := &Rect{W: 3, H: 4}
p.Scale(2) // ポインタレシーバなのでそのまま呼べる
fmt.Println(p.Area()) // コンパイラが (*p).Area() に読み替えるただし、リテラル・関数の戻り値・mapのインデックス式など、アドレスが取得できない値の場合は自動補完が効かず、ポインタレシーバのメソッドを直接呼び出すことができません。
// Rect{W: 1, H: 1}.Scale(2) // NG: リテラルはアドレスが取得できない
(&Rect{W: 1, H: 1}).Scale(2) // OK: &Rect{...}でリテラルから*Rectを作れる(変数に入れないので変更は捨てられる)
rects := map[string]Rect{"a": {}}
// rects["a"].Scale(2) // NG: mapのインデックス式はアドレスが取得できない
v := rects["a"]
v.Scale(2)
rects["a"] = v // 変数に取り出して変更し、mapへ戻すポインタレシーバのメソッドは、レシーバがnilであっても呼び出し自体は可能です。ただし、メソッド内でフィールドにアクセスした時点で nilポインタデリファレンスが発生しパニックになります。一方、値レシーバのメソッドをnilのポインタから呼び出すと、コンパイラが値をコピーしようとして(*p)の展開を行うため、呼び出しの時点でnilポインタデリファレンスが発生しパニックになります。
var p *Rect
// p.Area() // 値レシーバ: 呼び出し時に(*p)で値コピー → nilポインタデリファレンスでパニック
// p.Scale(2) // ポインタレシーバ: 呼び出しは成功するが、内部でr.Wを参照 → nilポインタデリファレンスでパニックメソッド式とメソッド値
メソッドは関数値として扱えます。
r := Rect{W: 3, H: 4}
fn := r.Area // メソッド値: レシーバがあらかじめ結び付いている
fmt.Println(fn())
area := Rect.Area // メソッド式: レシーバを第1引数で渡す
fmt.Println(area(r))埋め込みとメソッド昇格
構造体に別の型を埋め込むと、その型のメソッドが昇格して外側の型から呼び出せるようになります。外側の型に同名メソッドがある場合は引数が異なる場合でも外側が優先されます。
type Engine struct{}
func (e Engine) Start() {}
type Car struct {
Engine
}
var c Car
c.Start() // Engine.Start が昇格エラー
Go言語ではエラーを例外ではなく関数の戻り値として返します。
f, err := os.Open("file.txt")
if err != nil {
return err
}
// ここ以降は正常にファイルが開けている前提で処理できるerror型
errorは組み込みインターフェースです。Error() stringメソッドを持つ型であれば、errorとして扱えます。
type error interface {
Error() string
}エラーの生成
固定文言のエラー
動的な値を含まない静的なエラーにはerrors.Newを使用します。特定のエラーとして呼び出し側で判定できるよう、パッケージレベルの変数として定義する用途にも適しています。
var ErrNotFound = errors.New("not found")動的な値を含むエラー
実行時の数値や文字列をメッセージに埋め込む場合はfmt.Errorfを使用します。
return fmt.Errorf("item %d not found", itemID)下流エラーのラップ
発生したエラーに上位の文脈を追加しつつ、元のエラー値を辿れるようにする場合はfmt.Errorfで書式指定子%wを指定します。メッセージに文言だけ足す場合は%vを指定します。
return fmt.Errorf("open %s: %w", path, err)%wでラップしたエラーは、errors.Isを用いて内部のエラーと一致するか判定できます。%vではメッセージに文言は残りますが、元のエラー値は辿れないため一致しません。
var ErrNotFound = errors.New("not found")
wrapped := fmt.Errorf("load config: %w", ErrNotFound)
fmt.Println(errors.Is(wrapped, ErrNotFound)) // true
textOnly := fmt.Errorf("load config: %v", ErrNotFound)
fmt.Println(errors.Is(textOnly, ErrNotFound)) // false独自のエラー型
構造体にError() stringメソッドを実装すると、組み込みのerrorインターフェースを満たし、error型の戻り値として返せるようになります。エラーメッセージだけでなく、失敗した入力値やエラーコードなどをフィールドに保持させたい場合に適しています。
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q: %v", e.Query, e.Err)
}error型のゼロ値
errorのゼロ値はnilであり、err == nilであれば処理が成功したことを表します。非nilのエラー値が返された場合は失敗として扱います。
func mayFail(ok bool) error {
if ok {
return nil // 成功
}
return errors.New("failed")
}
if err := mayFail(true); err != nil {
// 失敗処理
} else {
// 成功処理
}具象型のnilポインタをerrorとして返すと、ポインタはnilでもインターフェースに型が残るためerr != nilになります。成功を返すときはnilをそのまま返します。
func load() error {
var perr *os.PathError
return perr // perrはnilだが、戻り値のerrorは非nil
}panic
panicは実行中の関数の処理を中断し、登録されたdeferを実行しながら呼び出し元の関数を順に遡ります。途中でrecoverで回復されなければ、プログラム全体が強制終了します。整数のゼロ除算やスライスの範囲外アクセスなどによりランタイムが自動的に発生させるほか、組み込み関数panicで明示的に発生させることもできます。明示的なpanicは、初期化の失敗で起動を継続できない場合や、到達してはならない分岐に達した場合など、続行不可能な異常に限って使用します。
func mustPositive(n int) int {
if n <= 0 {
panic("unreachable: positive number required")
}
return n
}recover
recoverは、panicによってプログラムが強制終了するのを防ぐ組み込み関数です。必ずdeferで登録した関数の内部で直接呼び出します。通常時はnilを返しますが、panicの発生中に呼び出すとパニックの伝播を止め、panicに渡された値を取得します。recoverを実行すると、panicが発生した箇所以降の処理は中断されたまま終了し、deferを登録した関数が呼び出し元へ戻ります。名前付き戻り値と組み合わせることで、パニックを捕捉して呼び出し元へerrorとして返すことができます。なお、recoverが有効なのは同一ゴルーチン内で発生したpanicに限られます。また、Go言語1.21以降では、panicにnilが渡された場合でもrecoverは非nilの値を返します。
func safeDivide(a, b int) (res int, err error) {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
err = fmt.Errorf("panic: %v", r)
}
}()
return a / b, nil
}
_, err := safeDivide(10, 0)
fmt.Println(err) // panic: runtime error: integer divide by zeroゴルーチン
ゴルーチンとは、Goランタイムが管理する軽量なスレッドです。関数の前にgoを付けて呼び出すと、その関数の完了を待たずに、すぐ次の文へ進みます。呼び出された関数は、新しいゴルーチンとして元の処理と並行して実行されます。OSスレッドより起動コストが小さく、多数を並行して動かせます。
done := make(chan struct{})
go func() {
fmt.Println("別のゴルーチンから")
done <- struct{}{}
}()
fmt.Println("main側")
<-donemain終了とゴルーチンの寿命
mainゴルーチンがreturnするとプログラム全体が終了し、他のゴルーチンも打ち切られます。mainが先に終わると、起動したばかりの処理の完了を待たずに終了します。
go fmt.Println("worker")
// mainがすぐ終わると、workerの出力が欠けることがある完了を待つには、チャネルやsync.WaitGroupなどの同期が必要です。
送信と受信
チャネルは値を送受信する型です。ゴルーチン間でよく使われます。要素型Tのチャネルはchan Tと書きます。ch <- vはチャネルchへvを送り、<-chはchから値を受け取ります。受信は式にも文にも書けます。x := <-chのように値を受け取るほか、<-chだけで値を捨てることもできます。
ch := make(chan string)
go func() {
ch <- "ping" // 送信
}()
msg := <-ch // 受信
fmt.Println(msg) // pingch <- v // 送信
x := <-ch // 受信
<-ch // 値を捨てる受信送受信の両方に使えるchan Tに対し、向きを片方に限った型もあります。chan<- Tは送信専用、<-chan Tは受信専用です。双方向のチャネルは、送信専用や受信専用の変数へ代入できます。合わない向きの操作はコンパイルエラーになります。
ch := make(chan int)
var send chan<- int = ch // (chan<- int)(ch)でも同じ
var recv <-chan int = ch // (<-chan int)(ch)でも同じ
go func() { send <- 1 }()
fmt.Println(<-recv) // OK
// <-send // NG: 送信専用から受信できない
// recv <- 1 // NG: 受信専用へ送信できない
// close(recv) // NG: 受信専用は閉じられないmake(chan int)はバッファなしチャネルです。送信と受信が揃うまで双方がブロックされます。
ch := make(chan int)
go func() {
ch <- 1 // 受信が来るまでここで待つ
}()
n := <-ch // 送信が来るまでここで待つ
fmt.Println(n) // 1ゴルーチンの送信と受信はどちらが先でも問題ありません。
ch := make(chan int)
go func() {
n := <-ch // 送信が来るまでここで待つ
fmt.Println(n) // 1
}()
ch <- 1 // 受信が来るまでここで待つバッファなしチャネルは、送る側と受ける側が揃う必要があります。同じゴルーチンだけで送受信すると、相手が現れずその場で止まります。他に動いているゴルーチンがなければ、ランタイムはデッドロックとしてプログラムを終了します。
ch := make(chan int)
ch <- 1 // 受信するゴルーチンがいない → デッドロック値を渡さず終わったことだけ伝えたいときは、要素型を空の構造体にしたチャネルが使えます。struct{}はフィールドを持たない型で、値そのものに伝える情報はありません。送受信が起きたこと自体が合図になり、chan boolのように使わない中身を載せる必要がありません。サイズも0なので、完了通知だけに向いています。
done := make(chan struct{})
go func() {
fmt.Println("worker")
done <- struct{}{} // 完了の合図
}()
<-done // mainが待つ → workerの出力を見られる値の受け渡しが不要で、複数のゴルーチンの終了だけ待つときはsync.WaitGroupが使えます。WaitGroupは内部で完了待ちのタスク数をカウントします。WaitGroup.Goに関数を渡すと、ゴルーチンの起動と完了の追跡をまとめて行います。待ち側でWaitを呼び、すべての処理が終わるまで待機します。
var wg sync.WaitGroup
wg.Go(func() { fmt.Println("worker", 1) })
wg.Go(func() { fmt.Println("worker", 2) })
wg.Wait() // すべてのゴルーチンが終わるまで待つ手動でAddとDoneを使う方法もあります。Addで待つタスクの数をカウンタに加算し、各ゴルーチンの終了時にDoneで1つ減らします。Addはゴルーチン起動より前に呼びます。起動後にAddすると、カウンタが増える前にWaitが待ち終わったと判断して先に進んでしまうことがあります。
var wg sync.WaitGroup
wg.Add(2)
for id := 1; id <= 2; id++ {
go func(id int) {
defer wg.Done()
fmt.Println("worker", id)
}(id)
}
wg.Wait()makeの第2引数が1以上のときバッファ付きチャネルになります。0や省略はバッファなしです。バッファに空きがあれば受信側を待たずに送信でき、値があれば送信側を待たずに受信できます。バッファが満杯のときは送信が、空のときは受信がブロックされます。
ch := make(chan int, 2)
ch <- 10 // 空きがあるのでブロックされない
ch <- 20
fmt.Println(<-ch, <-ch) // 10 20容量を超えて送ると、空きができるまで送信側が止まります。他に動いているゴルーチンがなければデッドロックになります。
ch := make(chan int, 1)
ch <- 1
// ch <- 2 // 受信が無いとここでブロックされる(他に動くゴルーチンがなければデッドロック)closeとrange
close(ch)は、これ以上値を送信しないことを伝える操作です。チャネルを閉じるのは基本的に送信側の役割です。受信側が閉じてしまうと、まだ値を送ろうとしている送信側が閉じたチャネルへ送信してしまい、パニックの原因になります。なお、closeは受信専用チャネル(<-chan T)には呼べません。また、すでに閉じたチャネルやnilチャネルを閉じてもパニックになります。
閉じたチャネルへ送ると、実行時にパニックします。
ch := make(chan int)
close(ch)
// ch <- 1 // panic: send on closed channel通常、空のチャネルから受信しようとすると値が届くまでブロックされますが、閉じたチャネルからは待たされずに値を取り出せます。バッファに残っている値があればそれを取り出し、すべて取り出し終えたあとはゼロ値とfalseが返ります。以降はブロックされず、何度受信してもゼロ値とfalseが即座に返り続けます。
ch := make(chan int, 1)
ch <- 7
close(ch)
v, ok := <-ch
fmt.Println(v, ok) // 7 true(バッファに残っていた値)
v, ok = <-ch
fmt.Println(v, ok) // 0 false(空になってもブロックされず、ゼロ値とfalseが返る)
v, ok = <-ch
fmt.Println(v, ok) // 0 false(何度受信しても同じ)range chはチャネルが閉じられるまで受信を続けます。送信側がcloseしないと、rangeは終わりません。
ch := make(chan string, 2)
ch <- "a"
ch <- "b"
close(ch)
for s := range ch {
fmt.Println(s) // a → b
}select
select文は複数の通信操作のうち、実行可能なcaseを1つ選びます。どれもすぐに実行できないときは、準備できるまで待ちます。複数のcaseが同時に準備できていれば、一様な疑似乱数でそのうち1つが選ばれます。並びの先頭が優先されるわけではありません。
defaultを付けると、どの通信もすぐできないときに待たずそのcaseへ進みます。
ch1 := make(chan string, 1)
ch2 := make(chan string, 1)
ch1 <- "from ch1"
select {
case msg := <-ch1:
fmt.Println(msg) // from ch1
case msg := <-ch2:
fmt.Println(msg)
default:
fmt.Println("no communication")
}default無しのselectで、どのcaseも永久に準備できないとそのゴルーチンは止まります。他に動いているゴルーチンがなければデッドロックになります。チャネル操作と同様に、相手側の送受信やcloseがある前提で書きます。
値の可視性
1つのゴルーチン内では、コードに記述された順序どおりに処理が実行されます。しかし複数のゴルーチンが共有メモリを読み書きする場合、同期を行わない限り、あるゴルーチンが行った書き込みがいつ別のゴルーチンから見えるかは保証されません。これは、コンパイラやCPUが実行性能を高めるために、プログラムの意味を変えない範囲で命令の並べ替えを行ったり、レジスタやCPUキャッシュに値を保持したりするためです。
次の例では、書き込み側のゴルーチンがmessageに値を設定したあとにreadyをtrueに更新しています。
var message string
var ready bool
go func() {
message = "done"
ready = true
}()
for !ready {
// readyがtrueになるのを待つ
}
fmt.Println(message) // 空文字列が表示される可能性がある直感的には、ループを抜けてready == trueが確認できたなら、その前に書かれたmessageも"done"になっていると期待されます。しかし同期が行われていない場合、コンパイラやCPUによる並べ替えによって、読み取り側から見るとready = trueの反映がmessage = "done"より先に見えることがあります。また、readyの更新自体がキャッシュからメモリへ反映されず、ループが終了しない可能性もあります。
happens-before関係
メモリモデルでは、プログラム内のメモリ操作の順序と可視性を規定するためにhappens-beforeという概念を用います。操作Aが操作Bに対してhappens-beforeである、つまり操作Aが操作Bより前のとき、操作Aによるメモリの書き込みは、操作Bから確実に読み取れることが言語仕様として保証されます。単一のゴルーチン内では、プログラムの記述順序に従ってhappens-before関係が成り立ちます。
go func() {
name := ""
name = "gopher"
fmt.Println(name) // 同じゴルーチン内なので必ずgopher
}()go文によるゴルーチンの起動は、そのゴルーチンの開始に対してhappens-beforeとなります。そのため、go文より前に行われた書き込みは、起動されたゴルーチンから確実に読み取ることができます。
name := "gopher"
go func() {
fmt.Println(name) // 必ずgopher
}()一方で、ゴルーチンの終了自体は他のいかなる操作に対してもhappens-before関係を持ちません。そのため、起動されたゴルーチン内で行われた書き込みは、チャネルやミューテックスなどで明示的に同期しない限り、起動元のゴルーチンから読み取れる保証はありません。
var name string
go func() {
name = "gopher"
}()
fmt.Println(name) // 空文字列のことがあるこのようにチャネルの送受信で同期を明示的に行うと、送信前の書き込みが受信後に必ず読み取れます。
var name string
done := make(chan struct{})
go func() {
name = "gopher"
done <- struct{}{}
}()
<-done
fmt.Println(name) // 必ずgophercloseは、送信と同じ向きの順序付けができます。閉じるより前の書き込みは、閉じたチャネルからの受信後に必ず読み取れます。
var name string
done := make(chan struct{})
go func() {
name = "gopher"
close(done)
}()
<-done
fmt.Println(name) // 必ずgopherバッファなしチャネルmake(chan T)では、送信側と受信側が揃って初めて通信が成立します。そのため送信の完了が受信の完了に対してhappens-beforeとなるだけでなく、受信の完了も送信の完了に対してhappens-beforeとなります。
一方、バッファ付きチャネルmake(chan T, n)では、バッファに空きがある限り、受信側を待たずに送信が即座に完了します。容量がnのバッファ付きチャネルで仕様上保証されるのは、k回目の受信完了は、k+n回目の送信完了に対してhappens-beforeとなるという規則です。
var name string
done := make(chan struct{}, 1)
go func() {
name = "gopher"
<-done
}()
done <- struct{}{}
fmt.Println(name) // 空文字列のことがあるnが1のとき、1回目の受信は2回目の送信の完了に対してhappens-beforeになります。2回目の送信はバッファが空くまで待つので、その完了後には受信より前の書き込みが見えます。
var name string
done := make(chan struct{}, 1)
go func() {
name = "gopher"
<-done
}()
done <- struct{}{}
done <- struct{}{}
fmt.Println(name) // 必ずgopher空きバッファへの送信は相手の受信を待たずに進むため、送信完了だけでは受信側の書き込みを読み取れる保証にはなりません。一方的な通知ではなく双方向の同期が必要な場合は、バッファなしチャネルを使うか、完了通知用のチャネルを別途設けます。
データ競合
2つ以上のゴルーチンがメモリ上の同じ位置へ同時にアクセスし、少なくとも1つが書き込みであり、かつそれらのアクセス間にhappens-before関係がない状態をデータ競合と呼びます。データ競合が起きると、意図した値が読めなくなるだけでなく、内部で複数の情報(ポインタや長さなど)を管理するstringやスライスでは値の破損やクラッシュにつながります。bool1つの代入であっても、同期のない並行アクセスはデータ競合になります。
var counter int
go func() {
counter++
}()
fmt.Println(counter) // データ競合: 0か1かは決まらないGo言語にはデータ競合を検出するレースディテクタが標準で備わっています。実行時やテスト時に-raceフラグを付与します。
go test -race ./...
go run -race main.goレースディテクタは実行時のメモリアクセスを監視して競合を報告します。テストで実行されなかったコードパスの競合は検出できないため、並行処理を行うすべての経路をテストで動かす必要があります。
sync.Mutexによる排他制御とhappens-before
複数のゴルーチンが共有メモリを読み書きする場合、sync.Mutexによるロックを使ってアクセスを排他制御します。sync.Mutexは同時に1つのゴルーチンしか保護区間に入れないようにするだけでなく、メモリの可視性に関するhappens-before関係も保証します。
あるゴルーチンによるUnlockの呼び出し完了は、その後に成功する任意のゴルーチンによるLockの呼び出しに対してhappens-beforeとなります。したがって、あるゴルーチンがUnlockを呼ぶ前に行ったすべてのメモリ書き込みは、次にLockの取得に成功したゴルーチンから確実に読み取れます。
var (
mu sync.Mutex
n int
)
mu.Lock()
go func() {
defer mu.Unlock()
n++
}()
mu.Lock()
fmt.Println(n) // 必ず1
mu.Unlock()モジュール
モジュールは、1つのアプリやライブラリをまとめる単位です。go.modが置かれたフォルダとその中身全体が1つのモジュールになります。go.modファイルはgo mod initコマンドで生成することができます。サブディレクトリごとにgo mod initする必要はありません。同じgo.modの下にあるパッケージは、すべてこのモジュールに属します。
mkdir demo
cd demo
go mod init example.com/demomodule example.com/demo
go 1.26go行は、このモジュールが想定するGo言語のバージョンです。
外部パッケージをソースコード上でimportし、go mod tidyコマンドを実行すると、外部ライブラリを自動で検知し、go.modの依存関係を自動で更新してくれます。
パッケージ
パッケージはコンパイルの単位であり、識別子の名前空間でもあります。通常、1つのディレクトリが1つのパッケージです。同じディレクトリの.goファイルには同じpackage句を書き、まとめてコンパイルします。
// greeter/greet.go
package greeter
func Hello(name string) string {
return "hello, " + name
}// main.go
package main
import (
"fmt"
"example.com/demo/greeter"
)
func main() {
fmt.Println(greeter.Hello("Go"))
}import
package句のあと、import宣言で外部から使用したいパッケージを読み込むことができます。
import "fmt"()を使ってまとめてimportすることもできます。
import (
"errors"
myio "io"
_ "image/png"
)別名を付けると、パッケージ名の衝突を避けられます。myio "io"ならmyio.Readerのように使います。
importしたパッケージを使用しないとコンパイルエラーになります。使用はしないけどパッケージの初期化だけ実行したいときは_ "image/png"のようにブランクimportします。
import . "fmt"は、パッケージ名なしで識別子を取り込む書き方です。通常ならfmt.Printlnと書くところを、Printlnだけで呼べます。
import . "fmt"
func main() {
Println("hello") // fmt.Println("hello")と同じ
}importパスとパッケージ名
importパスは、import "..."に書く文字列です。モジュールパスにディレクトリを足したもので、どのフォルダのコードを取り込むかを指定します。パッケージ名は、取り込んだあとにコード上で使う名前です。そのディレクトリの.goファイル先頭のpackage句で決まります。
// greeter/greet.go
package greeter
func Hello(name string) string {
return "hello, " + name
}import "example.com/demo/greeter" // importするときはディレクトリ名
greeter.Hello("Go") // 呼び出すときはpackage名公開と非公開
関数や定数といった識別子の公開範囲は、名前の先頭文字で決まります。大文字で始まる名前は他パッケージから参照でき、小文字で始まる名前は同じパッケージ内だけです。
package greeter
func Hello() string { // 他パッケージから呼べる
return hello()
}
func hello() string { // 同じパッケージ内だけ
return "hello"
}import "example.com/demo/greeter"
greeter.Hello()
// greeter.hello() // コンパイルエラー: 小文字は他パッケージから見えないinternalパッケージ
パスにinternalを含むパッケージは、そのinternalの親ディレクトリ以下からだけimportできます。
たとえばexample.com/demo/internal/storeは、example.com/demo/...配下からはimportできますが、別モジュールやexample.com/demoの外にあたるパスからはimportできません。公開したくない実装を内側に閉じるための仕組みです。
ドキュメントコメント
ドキュメントコメントは、トップレベルのpackage、const、var、type、funcの直前に、空行を挟まず書いたコメントです。関数内の変数や、関数の引数・戻り値名には、ドキュメントコメントはありません。
// ParseConfigは設定ファイルを読み込む。
func ParseConfig(path string) (*Config, error) {
// ...
}コメントと宣言のあいだに空行があると、ドキュメントにはなりません。
// このコメントはドキュメントにならない
func other() {}constやvarをまとめたときは、グループ全体へのコメントと、各識別子の行末コメントが使えます。構造体のフィールドとインターフェースのメソッドも、型のドキュメントの一部になります。
// Statusは処理の状態です。
type Status int
const (
Ready Status = 1 // 準備完了
Running Status = 2 // 実行中
)
// Serverは待ち受け設定です。
type Server struct {
Host string // 待ち受けホスト
Port int // 待ち受けポート
}init関数
パッケージ内にfunc init()を複数定義できます。initはインポートや変数の初期化のあと、自動的に実行されます。
func init() {
log.SetFlags(log.LstdFlags)
}パッケージの初期化順序
パッケージの初期化は次の順序です。
- インポートされたパッケージ(依存関係の順)
- パッケージレベルの変数(宣言順)
init関数(ソースファイル内の出現順)
フォーマット
Go言語のソースコードは、手で細かく揃えるのではなくgofmt(パッケージ単位ならgo fmt)で機械的に整形することができます。インデントはタブ、空白の入れ方や並びも標準形に揃います。
ファイルをその場で書き換える例です。
gofmt -w main.goパッケージ単位で揃えるときはgo fmtを使います。結果のスタイルはgofmtと同じです。
go fmt ./...テスト
テストは、対象と同じディレクトリに_test.goで終わるファイルを置きます。書き方は2通りあり、非公開の識別子をテストできるかどうかが異なります。
同じpackage greeterで書くと、非公開の識別子も含めパッケージ内部がすべて見えます。非公開の補助関数や内部状態まで検証したいときに使います。importは不要で、同じパッケージの関数をそのまま呼べます。
// greeter/greet_test.go
package greeter
import "testing"
func TestHello(t *testing.T) {
if Hello("Go") != "hello, Go" {
t.Fatal("unexpected greeting")
}
}package greeter_testで書くと、他パッケージからの利用と同じです。importが必要で、公開されている識別のみテストできます。
// greeter/greet_ext_test.go
package greeter_test
import (
"testing"
"example.com/demo/greeter"
)
func TestHello(t *testing.T) {
if greeter.Hello("Go") != "hello, Go" {
t.Fatal("unexpected greeting")
}
// greeter.hello() // コンパイルエラー: 他パッケージからは見えない
}依存の追加と利用
go getは、指定したモジュールを依存関係に追加し、ソースコードをマシン上のモジュールキャッシュへダウンロードします。モジュールキャッシュの既定の配置場所は$GOPATH/pkg/modで、複数のプロジェクト間で共有されます。プロジェクト直下にライブラリ用のディレクトリは作られず、依存関係はgo.modに記録されます。
go get rsc.io/quote取得したパッケージをコード内で使うには、ソースファイルでimportを宣言します。
package main
import (
"fmt"
"rsc.io/quote"
)
func main() {
fmt.Println(quote.Hello())
}go run .fmtなどの標準ライブラリはGo言語のツールチェーンに含まれているため、go getで追加する必要はなく、go.modにも記録されません。
go.modとgo.sum
依存を追加すると、go.modにrequireディレクティブが追加されます。ここに記録されるバージョンは固定値ではなく、ビルドに必要な最小バージョンです。依存先モジュールがより新しい最小バージョンを要求している場合は、すべての要求を満たすバージョンが自動で選ばれます。公開されている最新バージョンへ勝手に更新されるわけではありません。
module example.com/demo
go 1.26
require rsc.io/quote v1.5.2依存の追加に伴い、チェックサムを記録するgo.sumも作成または更新されます。ダウンロードしたモジュールの内容が将来にわたって改ざんされていないかを検証するためのファイルです。依存関係を定義するgo.modと、その完全性を保証するgo.sumは、どちらもソースコードと一緒にGitなどのバージョン管理に含めます。
requireの行末に付く// indirectは、現在のモジュールから直接importされていない間接依存を表します。依存先モジュールが内部で利用しているモジュールや、go getで追加したもののコード上でまだimportしていないモジュールに付与されます。ソースコードで直接importして利用を始めると、この表記は自動で外れます。手動で編集する必要はなく、go getやgo mod tidyによって管理されます。
go mod tidyによる同期
go getを使わずに、先にソースコードへimportを書いてから依存を同期する方法もあります。
import "rsc.io/quote"go mod tidygo mod tidyは、ソースコード内のimport宣言に合わせてgo.modとgo.sumを同期します。不足している依存を追加し、コード内で使われていない依存を削除します。そのため、依存をやめるときはソースコードから対象のimportと利用箇所を削除してgo mod tidyを実行します。
バージョンの指定
依存のバージョンを指定するには、モジュールパスの末尾に@とバージョンを付けます。
go get rsc.io/[email protected]
go get rsc.io/quote@latest@latestを指定すると、公開されている最新のリリースバージョンが選ばれます。リリースがなければv1.2.3-preのようなプレリリース、タグがなければ既定ブランチの最新コミットが選ばれます。
バージョンを省略してgo get rsc.io/quoteを実行した場合も、基本は@latestと同じです。すでに要求しているバージョンの方が新しいときだけ、手元を維持します。
どちらの場合も、go.modには解決された具体的なバージョン番号が記録されます。go.modに手動でlatestと書くことはできず、バージョンの更新はgo getで行います。
メジャーバージョンとimportパス
メジャーバージョンが2以上のモジュールは、パスの末尾に/v2や/v3のようなバージョン接尾辞が付きます。go getの引数だけでなく、コード内のimportパスにもこの接尾辞を含めます。
go get rsc.io/quote/v3@latestimport "rsc.io/quote/v3"
quote.HelloV3()接尾辞の異なるパスは別々のモジュールです。メジャーバージョンが違えば後方互換はなく、パスが分かれているため同一プロジェクト内で同時に使えます。
go installによるバイナリの配置
go installは、mainパッケージをコンパイルし、実行可能バイナリをマシン環境へ配置するためのコマンドです。
自分のプロジェクトで作成したコマンドをビルドして配置する場合は、バージョンを指定せずに実行します。現在のモジュールの依存関係に基づいてビルドされます。
go install .生成された実行ファイルは、GOBIN環境変数が設定されていればそのディレクトリに、未設定であれば$GOPATH/binに配置されます。パスを通しておくことで、ターミナルから直接コマンドを実行できます。
外部のCLIツールをマシンに導入する場合は、パスの末尾に@latestなどのバージョンを指定します。この形式では現在のディレクトリにあるgo.modを読み込まず、指定したバージョンで独立してビルドされます。
go install golang.org/x/tools/cmd/stringer@latest
stringer -help