Go言語基礎
2026/07/28

Go(Golang)は、静的型付け・ガベージコレクション・組み込みの並行処理を備えたコンパイル言語です。Googleによって開発され、2009年に公開されました。

言語の性質

ソースはコンパイルしてから実行し、型の不整合はビルド時に検出されます。メモリはランタイムのガベージコレクションが回収するため、ヒープの解放をプログラマが担う必要はありません。成果物は依存ライブラリを同梱した単一バイナリになりやすく、仮想マシンなしで配布できます。

並行処理向けに、ゴルーチンとチャネルが言語に組み込まれています。ゴルーチンはOSスレッドより起動コストが小さく、多数のゴルーチンを並行に走らせやすい設計です。失敗は例外ではなくerror型の戻り値で明示的に扱います。クラス継承はなく、構造体の埋め込みとインターフェースで振る舞いを組み立てます。

言語仕様が定義する領域

Go言語仕様は、どのようにプログラムを書けるかと、書いたプログラムが実行時にどう動くかを定めます。型の整合性や変換ルール、変数の初期化、ifforなどの制御、関数呼び出しやメソッドの振る舞いがここに含まれます。

プログラムのまとまりとしてはパッケージとモジュールが規定され、起動時の初期化順序やpanicの扱い、ゴルーチンとチャネルに関する同期の前提も仕様に書かれています。コンパイラとランタイムはこれらのルールに従い、仕様外の挙動は実装に委ねられます。

公式リソース

環境構築

go versionを実行してバージョンが表示されない、またはgo: command not foundと出るなら、まずツールチェーンを入れます。インストール後もgo versionで導入を確認し、プロジェクト直下にgo.modを置いてモジュールを初期化できる状態を整えます。

公式インストーラ

OS用のインストーラを入れます。

go version

go version go1.26.x ...のように、リリース履歴に載る最新の安定版が表示されれば成功です。

コンパイラが入ったら、プロジェクトの境界をgo.modで定義します。

モジュール

Go 1.16以降、モジュールモードがデフォルトです(モジュール自体はGo 1.11で導入)。プロジェクトルートのgo.modにモジュール名と依存が記録されます。それ以前はGOPATHという作業領域の下にソースと取得した依存を置き、importパスもそのツリーに合わせる必要がありました。GOPATH前提の古い手順のまま進めるとimportパスが合わなくなることがあるため、モジュールモードではgo.modがあるディレクトリをプロジェクトの基準にします。

mkdir hello
cd hello
go mod init example.com/hello

よく使うコマンド

コマンド説明
go run .ビルドして実行
go build実行ファイルを生成。-oで出力名を指定(例: go build -o hello .)。-o省略時はカレントディレクトリ名がそのまま実行ファイル名になる
go test ./...テストを実行
go fmt ./...整形

動作確認の例:

go mod init example.com/check
package main

import "fmt"

func main() {
    fmt.Println("toolchain ok")
}
go run .
go fmt .

import "fmt"を書いたままfmtを一度も使わないと、コンパイルエラーになります。Goは未使用のimportや変数を許さないため、「何を使うか」をコード上で明示する必要があります。実行可能プログラムを作るにはpackage mainfunc main()が必要ですが、ソースのファイル名はmain.goに固定されていません。hello.goのように任意の名前にできますが、慣習としてmain.goを使うことが多いです。

Hello, World

package main

import "fmt"

func main() {
    fmt.Println("Hello, World")
}
go run main.go

ソースの構成

先頭のpackage宣言、import、引数なしのfunc main()が、実行可能プログラムの骨格です。同一ディレクトリの.goファイルは同じpackage句で1つのパッケージに束ねられ、別ファイルで定義した関数も同じ名前空間から呼び出せます。

greet.goに処理を分け、main.gofunc 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"))
}

1ファイルだけのときはgo run main.goで足ります。複数ファイルがあるときにgo run main.goだけでは他ファイルが含まれないため、go run .でディレクトリごとビルドします。go runはビルド結果を一時的に実行するだけです。実行ファイルを残すときはgo build -o hello .のように-oで名前を付けて出力します。

字句・宣言とスコープ

ソースコードの表現

GoのソースコードはUTF-8でエンコードされたUnicodeテキストです。大文字と小文字は別の文字として扱われます。

コメント

コメントは、行コメント(//から行末まで)とブロックコメント(/*から*/まで)の2種類です。

// 行コメント

/*
ブロックコメント
*/

トップレベル宣言の直前に改行なしで置かれたコメントは、その宣言のドキュメントとみなされます。

// Add はaとbの和を返す。
func Add(a, b int) int {
    return a + b
}

識別子とキーワード

識別子は、変数・関数・型・パッケージなどに付ける名前です。文字で始まり、文字と数字からなります。アンダースコア_は、仕様上「小文字の文字」として扱われます。そのため識別子の先頭にも使え、先頭が_の名前は小文字始まりと同じく他パッケージからは見えません(公開されるのは先頭がUnicodeの大文字のときだけです)。

var _count int // 先頭が_なのでパッケージ外からは見えない
var count int  // 先頭が小文字でも同様に見えない
var Count int  // 先頭が大文字なら他パッケージから見える

次の語はキーワードであり、識別子には使えません。

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

セミコロンの自動挿入

文法上、Goの文はセミコロンで終わります。字句解析は、コンパイラがソース文字列をif:=などのトークンに分ける処理です。この段階で次のトークンの直後に改行やEOFがあればセミコロンを補うため、ソースに;を書く必要はほとんどありません。ソースに;を書いた場合もそれはセミコロンのトークンとして読まれ、行末が;の行では自動挿入は走らないため、二重にはなりません。

次のトークンの直後の改行でセミコロンが入ります。

  • 識別子、リテラル
  • break, continue, fallthrough, return
  • ++, --, ), ], }

if条件式の直後で改行すると、行頭の{の前にセミコロンが入ったと解釈されコンパイルエラーになります。制御構文の{は同じ行に書きます。

if x < 0 {
    return -x
}
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
)

複数の戻り値を、左辺の変数に同時に受け取れます。すでに宣言済みの変数への再代入は=です。

x, y = 1, 2
a, b = b, a

関数から複数の値を受け取るときも、宣言済みなら=で再代入します。

func swap(a, b int) (int, int) { return b, a }

x, y := 1, 2
x, y = swap(x, y) // 2, 1

命名の慣習

複数語の名前はアンダースコアではなくMixedCapsまたはmixedCapsを使います。短いスコープではiokのような短い名前もよく使われます。

ゲッターにはGetプレフィックスを付けない慣習があります。フィールドownerならメソッドはOwnerとします。

ブロック

ブロックは宣言と文のグループです。波括弧{ }で囲みます。関数本体、制御構文の本体、ifの初期化文を含む全体などがブロックを形成します。

スコープ

識別子は宣言されたブロックとその内側のブロックで有効です。内側のブロックで同名を宣言すると、外側の名前はそのブロック内では隠れます。

x := 1
{
    x := 2
    fmt.Println(x) // 2
}
fmt.Println(x) // 1

再宣言と:=

:=は、左辺に少なくとも1つ新しい変数があるときに使います。すでにそのブロックで宣言されている名前には代入され、それ以外は新たに宣言されます。

x, y := 1, 2
x, z := 3, 4 // xは代入、zは新規

ブランク識別子

_はブランク識別子です。主な用途は次の3つです。

  1. 値の破棄: _, err := do()
  2. 副作用のみのimport: import _ "image/png"
  3. コンパイル時の型チェック: var _ io.Reader = (*MyType)(nil)

未使用importはコンパイルエラーです。_でimportすると、パッケージのinitだけが実行され、名前は使いません。

ラベルのスコープ

ラベルは、Outer:のように文の前に付ける名前です。入れ子のループで外側へ抜けるときはbreak Outer、外側ループの次の反復へ進むときはcontinue Outerと書きます。gotoの飛び先にも使います。

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
    }

ラベルはそれを含む関数内でのみ有効です。別の関数のラベルには飛べません。

ゼロ値

宣言だけして値を入れない変数にはゼロ値が入ります。

ゼロ値
数値0
boolfalse
string""
ポインタ、スライス、マップ、チャネル、関数、インターフェースnil
構造体各フィールドのゼロ値
配列各要素のゼロ値

未初期化変数

Goには「未初期化のローカル変数」がありません。宣言された識別子は常にゼロ値か、明示的な初期化値を持ちます。これにより、読み取り前の不定値によるバグを減らす設計です。

ポインタ・スライス・マップ・チャネル・関数・インターフェースのゼロ値はnilです。nilは型ごとに「まだ使うための内部のデータ構造が用意されていない」状態を表します。操作がその内部構造への読み書きを前提にするとパニックになります。内部構造がなくても意味がある操作や、必要ならこちらで確保する組み込み(appendなど)は動くことがあります。

nilだからどの操作もパニックになるわけではなく、型と操作の組み合わせで決まります。たとえばnilマップへの書き込みはハッシュ表が無いためパニックですが、nilスライスへのappendは要素を格納する配列を新しく確保して動きます。マップを書き込みに使うにはmakeかリテラルでの初期化が必要です。

var s []int
s = append(s, 1) // 要素を格納する配列を確保して動く

var m map[string]int     // m == nil
// m["x"] = 1            // ハッシュ表が無いのでpanic
m = make(map[string]int) // または m = map[string]int{}
m["x"] = 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
)

パッケージレベルの変数初期化のあと、initが自動実行されます。

init関数

パッケージ内にfunc init()を複数定義できます。initはインポートや変数の初期化のあと、自動的に実行されます。

func init() {
    log.SetFlags(log.LstdFlags)
}

initに重い副作用やグローバル状態の変更を詰め込むと、テストやimport順序に影響しやすくなります。パッケージの最低限の登録に留めるのが無難です。

パッケージの初期化順序

パッケージの初期化は次の順序です。

  1. インポートされたパッケージ(依存関係の順)
  2. パッケージレベルの変数(宣言順)
  3. init関数(ソースファイル内の出現順)

例:

main が import → config
config が import → log

実行順: logの変数 → loginitconfigの変数 → configinitmainの変数 → maininitmain.main

同一パッケージ内では、コンパイル単位(ファイル)の並びと各ファイル内の出現順に依存します。初期化順に依存する設計は避けます。

mainパッケージの初期化が終わるとmain関数が呼ばれます。

基本型

数値型

整数型には符号付きのint8int16int32int64int、符号なしのuint8uint16uint32uint64uintuintptrがあります。日常の計数にはintを使うことが多いです。

浮動小数点はfloat32float64です。複素数はcomplex64complex128です。

整数リテラルは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のような整数リテラルは、代入先がfloat64float32ならその浮動小数点型として解釈されます。

f := 3.14159
e := 1.5e-10
imag := 2i

var x float64 = 1 // 1はfloat64になる
var y float32 = 2 // 2はfloat32になる

論理型

booltruefalseのみを取ります。

文字と文字列

byteuint8の別名、runeint32の別名でUnicodeコードポイントを表します。

ルーンリテラルはシングルクォートで囲み、1つのUnicodeコードポイントを表します。

r := 'あ'
newline := '\n'

stringはイミュータブルなバイト列です。ソースはUTF-8です。

文字列リテラルはダブルクォートまたはバッククォートで囲みます。バッククォート文字列は改行を含む生の文字列です。

s := "hello"
raw := `line1
line2`

文字列はrangeでルーン単位に走査できます。

for i, r := range "Go言語" {
    fmt.Printf("%d: %c\n", i, r)
}

実行結果(iはバイト位置、rはルーン。日本語はUTF-8で1文字が複数バイトのため、次のインデックスが飛ぶ):

0: G
1: o
2: 言
5: 語

byteruneは独立した型名ではなく、uint8int32の別名です。

s := "Go言語"
b := []byte(s)
fmt.Println(b)
fmt.Println(string(b))

var ch byte = 'A' // uint8 の別名
var r rune = '語' // int32 の別名
fmt.Printf("%T %c\n", ch, ch)
fmt.Printf("%T %c\n", r, r)

実行結果:

[71 111 232 168 128 232 170 158]
Go言語
uint8 A
int32 語

[]byte(s)string(b)でバイト列と文字列を相互変換できます。文字列はイミュータブルで、内容を書き換えられません。[]byteはミュータブルで、要素を書き換えられます。[]byte(s)は常にコピーします。スライスで要素を書き換えても、元の文字列は変わりません。string(b)は、変換後もbを書き換える可能性があるときはコピーします。コピーされていれば、bを変更しても得られた文字列の内容は変わりません。変換後にbをもう使わないとコンパイラが判断できる場合は、実装によってコピーを省略できることがあります(省略しても得られる文字列の値は同じです)。

コピーの有無は、スライス側を書き換えたあと元の文字列や得られた文字列がどう見えるかで確認できます。

s := "hello"
b := []byte(s)
b[0] = 'J'
fmt.Println(s)         // hello(s は変わらない)
fmt.Println(string(b)) // Jello

b2 := []byte{'h', 'i'}
t := string(b2)
b2[0] = 'H'
fmt.Println(t)          // hi(t は変わらない)
fmt.Println(string(b2)) // Hi

実行結果:

hello
Jello
hi
Hi

複合型の概要

説明
配列[n]T長さ固定
スライス[]T可変長のビュー
マップmap[K]Vキーと値の集合
構造体structフィールドのまとまり
ポインタ*T値への参照
関数第一級の値
インターフェースメソッド集合の抽象
チャネルchan Tゴルーチン間通信

関数型

関数型はパラメータと結果の型のリストで書きます。

type Handler func(int) error
var fn Handler = func(n int) error { return nil }

チャネル型

chan Tは型Tの値を送受信するチャネルです。chan<- Tは送信専用、<-chan Tは受信専用です。

ch := make(chan int)
sendOnly := (chan<- int)(ch)
recvOnly := (<-chan int)(ch)

型定義と型エイリアス

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
var t float64 = float64(c)

// var x float64 = i // コンパイルエラー: 暗黙変換はない

ポインタ型

ポインタ型*Tは型Tの値を指す参照です。

x := 42
p := &x
*p = 100
fmt.Println(x) // 100

値渡しとポインタ

  • ポインタ演算(p + 1など)はできません
  • ポインタのゼロ値はnilで、nilの間接参照はパニックです
  • 関数の引数は値渡しです。呼び出し元を変更したいときだけポインタを渡します
var p *int      // ゼロ値はnil
fmt.Println(*p) // panic: nilポインタの間接参照

関数にintを渡すと、その時点の値のコピーが引数に入ります。関数内で引数を書き換えても、呼び出し元の変数は変わりません。呼び出し元を書き換えたいときは&vでアドレスを渡し、関数内で*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) // 0

すべてをポインタにする必要はありません。小さな値型はコピーのまま渡すことが多いです。Goは常に値渡しです。ポインタを渡しても渡るのはポインタ値のコピーで、指す先は共有されます。

newによる割り当て

new(T)は型Tのゼロ値を置いた領域を確保し、そのアドレス*Tを返します。*int*Userのように、ゼロ値へのポインタが欲しいときに使います。var n int&nで同じポインタを得られますが、newなら中間変数を別に用意せず、1つの式で*Tを得られます。次の2つは同じ意味です。

p := new(int)
*p = 42

var n int
q := &n
*q = 42

スライス・マップ・チャネルは内部の初期化が必要なため、newでは使える状態になりません。これらはmakeで作ります。

配列型

配列[n]Tは長さnが型の一部です。[3]int[4]intは別の型で、相互代入できません。配列とスライスを同じだと考えがちですが、配列の長さは型に固定され、スライス[]Tは長さ可変のビューです。

配列は値型

配列を代入・引数渡しすると、要素全体がコピーされます。スライスも値渡しですが、コピーされるのはヘッダ(元配列への参照・len・cap)だけで、要素が並ぶ元配列は共有されます。大きな配列をそのまま渡すと要素まで丸ごとコピーされるため、可変長の処理にはスライス[]Tを使うことが多いです。

arr := [3]int{1, 2, 3}
dup := arr // 配列全体のコピー
dup[0] = 0
fmt.Println(arr[0], dup[0]) // 1, 0

構造体型

構造体はフィールドの名前付き集合です。

type Point struct {
    X, Y float64
}

複合リテラル

p1 := Point{1, 2}        // Point値(位置で X, Yに対応)
p2 := Point{X: 1, Y: 2}  // Point値(フィールド名付き)
p3 := &Point{X: 1, Y: 2} // *Point

// p3と同じ意味
p := Point{X: 1, Y: 2}
p4 := &p // *Point

p1p2は構造体の値そのものです。p3は複合リテラルで作った値のアドレスを、その場で取った*Pointです。pを用意してp4 := &pとする書き方も同じ意味で、p3p4は同じ型・同じ内容のポインタになります。違いは、p3なら1つの式で初期化とアドレス取得が終わることです。

*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)    // 昇格: c.Engine.Powerと同じ
var e Engine = c.Engine // OK
// var e2 Engine = c    // コンパイルエラー: CarはEngineではない

CarEngineとして代入することはできません。型の関係は「中に持っている」だけです。埋め込みを継承と考えると、インターフェース実装の判定を誤解しやすくなります。実装の有無は外側型のメソッド集合で判定されます。

比較

すべてのフィールドが比較可能なときだけ==で比較できます。スライス、マップ、関数を含む構造体は比較できません(マップのキーにも使えません)。

a := Point{X: 1, Y: 2}
b := Point{X: 1, Y: 2}
fmt.Println(a == b) // true

type Record struct {
    Tags []string // スライスを含むため比較不可
}
// var r1, r2 Record
// fmt.Println(r1 == r2) // コンパイルエラー

スライスとは

スライス([]T)は、同じ型の要素を順序付きで並べた可変長の列です。配列[n]Tと違い、長さは実行中に変えられます。

スライスの内部構造

スライス値は、要素が並ぶ元配列への参照、現在見えている要素数(len)、参照先配列の容量(cap)の3つから成るヘッダです。capは再割り当てなしに要素を追加できる上限を表します。

スライスを代入したり関数に渡したりすると、ヘッダがコピーされます。元配列は共有されるため、一方で要素を書き換えると他方からも見えます。s[low:high]の部分スライスも、同じ元配列の一部を指します。

スライスの作成

nums := []int{1, 2, 3}
buf := make([]byte, 0, 64) // len=0, cap=64

リテラルやmakeで作ります。要素の追加はappendで行います。

スライシングと共有

s[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]

midallは別のヘッダですが、要素は同じ元配列を指します。mid側を書き換えるとallからも見えます。

2要素式の容量はcap(元) - lowです。allのcapは5、lowは1なので、midのcapは4です。

fmt.Println(len(mid), cap(mid)) // 3, 4

3インデックス形式s[low:high:max]は、capをmax - lowに制限します。見えている範囲の末尾までcapを合わせたいときに使います。

fullCap := all[1:4:4] // len=3, cap=3(max-low = 4-1)
fmt.Println(len(fullCap), cap(fullCap)) // 3, 3

部分スライスへappendしたとき、元のスライスからも追記が見えるかどうかは、そのスライスのcapに空きがあるかで決まります。

all := []int{0, 1, 2, 3, 4}
wide := all[1:4] // len=3, cap=4(あと1要素分の空きあり)
wide = append(wide, 9)
fmt.Println(all) // [0 1 2 3 9](元配列に書き込まれた)

all = []int{0, 1, 2, 3, 4}
tight := all[1:4:4] // len=3, cap=3(空きなし)
tight = append(tight, 9) // 別配列に再割り当て
fmt.Println(all)         // [0 1 2 3 4](allは変わらない)

共有を断ちたいときは、makeで新しい元配列を確保し、copyで要素だけをコピーします。dstmidは別の元配列を指すため、dstを書き換えてもmidallは変わりません。

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は変わらない)

nilスライスと空スライス

lencap== nil
var s []int00true
s := []int{}00false
make([]int, 0)00false

いずれもappendは可能です。

appendのルール

appendは次のように動きます。

  1. 現在のcapに余裕があれば、元配列に書き込み、lenを増やした新しいヘッダを返す
  2. capが足りなければ、より大きな新配列を確保し、要素をコピーしてから追記する

appendはlenが増えた新しいヘッダを返します。戻り値を変数へ代入しないと、その変数のlenは変わりません。append(s, x)と書いただけではsは更新されません。

nums := []int{1, 2, 3}
nums = append(nums, 4, 5) // 末尾に要素4と5を追加 → [1, 2, 3, 4, 5]

s := []int{1}
append(s, 2)       // 戻り値を捨てる → len(s)は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は変わらない)

v = append(v, 99) // 戻り値をvへ代入すると反映
fmt.Println(len(v)) // 2

関数に渡るのはヘッダのコピーです。関数内でs = append(s, x)しても、呼び出し元の変数へ戻ったヘッダは更新されないため、呼び出し元から見たlenは変わりません。ただしヘッダには元配列への参照も含まれます。capに余裕があり再割り当てが起きない場合は、元配列へ書き込まれることがあります。

func grow(s []int) {
    s = append(s, 99) // 関数内のヘッダだけlen=2になる
}

data := make([]int, 1, 2) // len=1, cap=2
grow(data)
fmt.Println(len(data)) // 1のまま
fmt.Println(data[1])   // 99(元配列に書き込まれている)

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)
}

マップとは

マップ(map[K]V)はキーから値を引く連想配列です。

比較可能なキーだけ

キー型Kは比較演算==!=が定義されている型に限られます。スライス、マップ、関数は比較できないためキーに使えません。構造体はすべてのフィールドが比較可能なときだけキーにできます。

マップの作成と操作

makeで空のマップを作り、キーへ値を代入します。

scores := make(map[string]int) // map[string]int{"go": 90} でも初期化できる
scores["go"] = 90
scores["rust"] = 85

存在するキーはscores[k]で読み取れます。キーがあるかどうかを区別したいときは、2値受け取りでokを使います。okfalseならキーは無く、vは値型のゼロ値です。

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(goはゼロ値)

var m map[string]intのままではnilマップです。書き込みにはmakeかリテラルでの初期化が必要です。

var m map[string]int
// m["k"] = 1     // panic: nilマップへの書き込み

v, ok := m["k"]  // v=0, ok=false(読み取りは可)
delete(m, "k")   // キーが無くても安全

走査

マップの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の順(キー名で昇順)
}

スライスをrangeするとき、走査中にappendなどでスライス自体を変更すると、意図しないループ回数や要素の見落としが起きます。次のように書かないでください。

// 悪い例: 走査中にスライスを変更する
s := []int{1, 2, 3}
for i, v := range s {
    s = append(s, v) // ループ回数や処理対象が不定になる
}

値の表現

Tには、その型として有効な値の集合があります。boolならtruefalseint8なら-128から127までの整数だけがint8の値です。変数に入る値は必ずこの集合のどれか1つで、型は「どんな値が存在しうるか」を決めます。

基底型と型定義

各型には基底型(underlying type)があります。事前宣言されたブール・数値・文字列型や型リテラルの基底型は、その型自身です。型定義type T Uでは、Tの基底型はUの基底型と同じになります。型エイリアスtype T = UではTUは同一型であり、基底型も同じです。

type ID int
type Count = int // エイリアス: Countとintは同一型

型定義は新しい型を作る

type Celsius float64float64と別の型です。基底型が同じでも、代入には明示的な変換が必要になります。型定義は意味の違う値を混同しないための仕組みです。

var c Celsius = 36.5
var f float64 = float64(c) // OK
// var f float64 = c       // コンパイルエラー

type ID int(型定義)とtype Count = int(エイリアス)では代入や変換の可否が異なるので、構文を混同しないようにします。

型の同一性

名前付き型(type T Uで定義した型)は、常に他の型と別です。型エイリアスtype T = UUと同一型です。

複合型は、配列・スライス・構造体・ポインタ・関数・インターフェース・マップ・チャネルです。typeで別名を付けていない無名の複合型は、種類と内部の型構成が一致すれば同一型とみなされます。配列では要素型に加えて長さも型の一部になり、構造体ではフィールドの名前・型・並びが一致している必要があります。

  • []int[]intは同一
  • type A inttype B intは別(基底型は同じでも非同一)
  • []int[3]intは別(配列の長さは型の一部)
  • struct{ x int }struct{ y int }は別(フィールド名が異なる)
  • Stack[T any][]Tのように型パラメータTを含む型は、具体の型引数を与えてインスタンス化したあとに同一性が決まる(例: Stack[int]Stack[string]は別)

代入可能性

vを型Tの変数に代入できるかは代入可能性で決まります。代入可能性はコンパイル時に決まり、型の関係(同一性やインターフェースのメソッド集合など)を見ます。

主な規則:

  • 同一型どうしは代入可能
  • 基底型が同一の型定義どうしは、明示的変換を経て代入可能
  • インターフェース変数には、そのインターフェースを実装する非インターフェース型の値を代入可能
  • nilはポインタ、スライス、マップ、チャネル、関数、インターフェースに代入可能

表現可能性

定数や型変換の文脈で、値が型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) // コンパイルエラー: 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)   // コンパイルエラー: 文字列全体はbyte1個に表現できない

型付き定数は宣言時点でその型に表現できる値だけが許されます。範囲外は定数宣言自体がエラーになります。

const max int8 = 127  // OK
// const bad int8 = 300 // コンパイルエラー: 300はint8に表現できない

変数の型変換でも同じ判定が使われます。int8(300)のように、定数でない式を変換するときは実行時の値が範囲外でもコンパイルは通る場合がありますが、定数を変換するときはコンパイル時に表現可能性がチェックされます。

メソッド集合

表現可能性が定数の値の範囲を見るのに対し、メソッド集合は型ごとにコンパイラが呼び出し可能とみなすメソッドの一覧を見ます。インターフェース変数に何を入れられるかは、この一覧で決まります。

値型とポインタ型で一覧が違う

type Box struct{ size int }
func (b Box) Size() int { return b.size }  // 値レシーバ
func (b *Box) Grow(n int) { b.size += n }  // ポインタレシーバ

// Boxのメソッド集合:  Sizeだけ
// *Boxのメソッド集合: SizeとGrow

*Boxのメソッド集合はBoxのものを常に含みます。値レシーバのメソッドはポインタ型の一覧にも入るためです。

メソッド集合に含まれるメソッド
値型TレシーバがTのメソッド
ポインタ型*TレシーバがTまたは*Tのメソッド

インターフェースに入れられるか

Box*Boxでは一覧が違うので、要求するメソッドによって代入可否が変わります。代入する値の型(Box*Boxか)のメソッド集合を求め、インターフェースが要求するメソッドがすべて含まれるかを見ます。

type Measurer interface { Size() int }
type Expander interface { Grow(int) }

var m Measurer = Box{}    // OK: BoxにSizeがある
// var e Expander = Box{} // コンパイルエラー: BoxにGrowがない
var e Expander = &Box{}   // OK: *BoxにGrowがある

呼び出しと代入では判定が違う

変数bからGrowを呼ぶとき、bのアドレスが取得できる(&bが書ける)なら、コンパイラは(&b).Grow(...)に置き換えます。リテラル、関数の戻り値、boxes["a"]のようなmapのインデックス式はアドレスが取得できないため、置き換えは使われません。

var b Box
b.Grow(1) // OK: 変数はアドレスが取得できるので(&b).Grow(1)と解釈される

// Box{}.Grow(1) // コンパイルエラー: リテラルはアドレスが取得できない
(&Box{}).Grow(1) // OK: &Box{}でリテラルから*Boxを作れる(変数に入れないので変更は捨てられる)

boxes := map[string]Box{"a": {}}
// boxes["a"].Grow(1) // コンパイルエラー: mapのインデックス式はアドレスが取得できない
b = boxes["a"]
b.Grow(1)
boxes["a"] = b // 変数に取り出して変更し、mapへ戻す

インターフェースに入れるときは置き換えは行われず、入れる値の型Boxのメソッド集合だけで判定します。インターフェース型のメソッド集合は、宣言されたメソッドと埋め込まれたインターフェースのメソッドの和集合です。

// var e Expander = b // コンパイルエラー: 入れる値の型はBoxのまま

インターフェース型

インターフェース型は、具体型を指定せず「呼び出せるメソッドの条件」だけを書いた型です。条件を満たす値なら、中身の具体型が違っても同じ変数に入れられます。

type Puller interface {
    Pull(p []byte) (n int, err error)
}

メソッドを1つも宣言しないインターフェースは空インターフェースと呼ばれ、任意の型(インターフェース型を含む)を格納できます。anyは空インターフェースの別名です。anyに何でも入れられるから型安全、とはならず、型アサーションや型スイッチなしに具体型のメソッドやフィールドへアクセスできません。

インターフェース値の中身

インターフェース変数は、型の見え方が2段階あります。プログラムを書いてコンパイルするときに決まる静的型と、プログラムが動いて値を代入したときに決まる動的型・動的値です。静的型は変数宣言の型で、実行中も変わりません。

静的型(static type)

var r io.Readerio.Readerが静的型です。コンパイラはrからReadなど、io.Readerで許されたメソッドだけ呼べると判断します。この判断はビルド時に行われ、実行中にrの中身が*os.Fileでも別の型でも、静的型はio.Readerのままです。

動的型(dynamic type)

代入のたびに決まる、実際に入っている具体型です。r*os.Fileを入れれば動的型は*os.File、別の値を入れ直せば動的型も変わります。

動的値(dynamic value)

代入のたびに決まる、その具体型の値そのものです。*os.Fileを入れていれば、ファイルオブジェクトへのポインタ(またはnil)が動的値です。

静的型はコンパイラ向けの約束、動的型と動的値は実行時の中身です。nilの判定で混乱しやすいのは、動的型だけ付いて動的値がnilのときです。

暗黙の実装

implementsのような宣言はありません。型がインターフェースが要求するメソッドをすべて備えていれば、宣言なくそのインターフェースを実装したとみなされます。コンパイラは代入時にメソッド集合の包含関係をチェックします。値型Tとポインタ型*Tではメソッド集合が異なるため、代入する値の型にも注意が必要です。

type Puller interface {
    Pull(p []byte) (n int, err error)
}

type File struct{}

func (f File) Pull(p []byte) (int, error) {
    return 0, nil
}

var p Puller = File{} // FileがPullerのメソッドを持つので代入可能

nilの二段階

インターフェース変数のr == nilは、動的型と動的値の両方がないときだけtrueになります。ポインタ変数のf == nilとは別の判定です。

動的型も動的値もない

ゼロ値のインターフェース変数は、中身のペアが空です。

var r io.Reader
fmt.Println(r == nil) // true

動的型だけある

nilポインタをインターフェースに入れると、動的型は*os.File、動的値はnilのペアが入ります。ポインタ自体はnilでも、インターフェースとしては空ではないためr2 == nilfalseです。

var f *os.File
fmt.Println(f == nil) // true: ポインタ変数としてはnil

var r2 io.Reader = f
fmt.Println(r2 == nil) // false: 動的型 *os.File がある

中身がnilか調べる

インターフェースのnil判定だけでは、中身のポインタがnilか分かりません。型アサーションで取り出してから調べます。

if f, ok := r2.(*os.File); ok {
    fmt.Println(f == nil) // true: 中身のポインタはnil
}

埋め込み

インターフェース定義に別のインターフェース型名を並べると埋め込みになります。埋め込んだ側は、内側のインターフェースが要求するメソッドをすべて引き継ぎます。構造体の埋め込みのようにフィールドが昇格するわけではなく、メソッドの条件が合成されます。

type Puller interface {
    Pull(p []byte) (n int, err error)
}
type Pusher interface {
    Push(p []byte) (n int, err error)
}

type Duplex interface {
    Puller
    Pusher
}

要求されるメソッド

DuplexPullPushの両方を要求します。Duplexのメソッド集合はPullerPusherの和集合です。次のようにメソッドを直書きしたのと同じ意味です。

type Duplex interface {
    Pull(p []byte) (n int, err error)
    Push(p []byte) (n int, err error)
}

具体型と代入

PullPushの両方を備えた型はDuplexにもPullerにもPusherにも代入できます。どちらか一方だけの型はDuplexを満たしません。

type Pipe struct{}

func (Pipe) Pull(p []byte) (int, error) { return 0, nil }
func (Pipe) Push(p []byte) (int, error) { return len(p), nil }

type OneWay struct{}

func (OneWay) Pull(p []byte) (int, error) { return 0, nil }

var d Duplex = Pipe{}   // OK
var pull Puller = Pipe{}
var push Pusher = Pipe{}
// var bad Duplex = OneWay{} // コンパイルエラー: Push がない

命名の慣習

1メソッドのインターフェースは、メソッド名に-erを付けた名前にすることが多いです。Reader, Writer, Stringerなどが例です。

小さなインターフェース

インターフェースは必要なメソッドだけを宣言するのが慣習です。io.Readerのように1メソッドのインターフェースは、多くの型で再利用しやすくなります。

型アサーション

インターフェース値から具体型を取り出すには型アサーションを使います。s, ok := v.(T)の2値形式で取り出し、動的型がTでなければokfalseになります。

var v any = "hello"
s, ok := v.(string)
if !ok {
    // 動的型がstringでない
    return
}
// ここではsをstringとして使える

okを受け取らないs := v.(T)は、動的型がTでないときにパニックします。ユーザー入力や外部データから復元するときは、必ず2値形式で分岐します。

型スイッチ

複数の動的型を分岐するときはswitch v := x.(type)の型スイッチを使います。動的型に応じてcaseが選ばれるだけで、どのcaseにも当てはまらなくてもパニックしません。

func show(x any) {
    switch v := x.(type) {
    case string:
        fmt.Println("string:", v)
    case int:
        fmt.Println("int:", v)
    default:
        fmt.Println("other type")
    }
}

インターフェース実装の確認

暗黙の実装では、コンパイラは代入が起きたときにメソッド集合を見てインターフェースを満たすか判断します。型定義の近くにインターフェース変数への代入を書いておくと、メソッドの追加漏れやレシーバ型の取り違えをビルド時に検知できます。

書き方

型定義の直後に、満たすべきインターフェース型への代入を書きます。

type Pipe struct{}

func (Pipe) Pull(p []byte) (int, error) { return 0, nil }

var _ Puller = Pipe{}

var _ Puller = ...は通常の変数宣言と代入であり、コンパイル時に代入可能性がチェックされます。左辺の_は値を使わない識別子です。右辺にPipe{}(*Pipe)(nil)を置けるのは、実際に使うインスタンスではなく代入可能性の確認だけが目的だからです。

値型とポインタ型

代入の判定は、右辺の型のメソッド集合で行われます。ポインタレシーバのメソッドだけを持つ型では、値型ではなくポインタ型を右辺に置く必要があります。

type Sink struct{}

func (s *Sink) Pull(p []byte) (int, error) { return 0, nil }

// var _ Puller = Sink{}      // コンパイルエラー: SinkにはPullがない
var _ Puller = (*Sink)(nil) // OK: *Sinkのメソッド集合にPullがある

Sinkの例では次のようになります。

右辺Pullerへの代入
Sink{}Sinkゼロ値の構造体不可(SinkPullがない)
(*Sink)(nil)*Sinknil可(*SinkPullがある)

(*Sink)(nil)は実体を作らず、右辺の型が*Sinkであることだけをコンパイラに伝えます。代入可能性の確認だけが目的なので、インスタンスの代わりに型付きのnilを置けます。

PipePullは値レシーバなので、Pipe{}でも(*Pipe)(nil)でも代入できます。

いつ書くか

ライブラリで標準的なインターフェース(io.Readerなど)を実装するとき、型の直後に置くことが多いです。インターフェース側にメソッドが追加されても、実装型でコンパイルエラーになります。すべての型に必須ではなく、どこにもインターフェースへの代入がなく、契約を明示したいときの手段です。

複数のインターフェースを満たすことをまとめて宣言することもできます。

var (
    _ Puller = Pipe{}
    _ Pusher = Pipe{}
)

型パラメータと制約

ジェネリクス(Go 1.18以降)は、関数や型定義に型パラメータを書き、具体の型に当てはめて使い回す仕組みです。Tのような名前は、関数を呼び出すときや型引数を与えるときに、intなどの具体型に置き換わります。型パラメータは単独では書かず、角括弧内で制約とセットになります。[T any]Tが型パラメータ、anyが制約です。

型パラメータの構文

func Min[T cmp.Ordered](a, b T)type Stack[T any] structのように、関数・型どちらも名前の直後の[T 制約]に型パラメータと制約を並べて書きます。

制約と演算

制約は型引数として許容する型の集合を表し、関数内で使える演算やメソッドも制約で決まります。制約をanyにすると型引数は何でもよい一方、必要な演算が使えないことがあります。

func Min[T any](a, b T) T {
    if a < b { // コンパイルエラー: Tがanyでは<が使えない
        return a
    }
    return b
}

比較には<>が使える型向けの制約が必要です。

import "cmp"

func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

制約の書き方

制約はインターフェースとして書きます。定義した制約名は、anycmp.Orderedと同じく、型パラメータの角括弧内に書きます。

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 | ~int64 | ~float64は「3つのどれかに当てはまる型」という和集合です。intint64float64のいずれかを型引数にできます。

~は基底型(underlying type)を指定します。~intは「基底型がintの型」を集合に含めます。type MyInt intNumberを満たすので、Sum([]MyInt{1, 2, 3})のように呼び出せます。intそのものも~intに含まれます。

any

組み込みのanyは制約なしです。Min[T any]のように、どの型でも型引数にできますが、関数内で自由に演算できるわけではありません。

comparable

組み込みのcomparable==!=が使える型向けです。マップのキー型などに使います。

cmp.Ordered

cmp.Ordered<>などの順序比較が使える型向けです。Minの修正例ではこの制約を使いました。

制約をanyに緩めすぎると必要な演算やメソッドが使えずコンパイルエラーになるため、「この型パラメータで何をするか」に合わせて絞ります。

ジェネリック型

型定義では、フィールドやメソッドのレシーバ型に同じ型パラメータを書きます。

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(v T) {
    s.items = append(s.items, v)
}

Stack[int]のように型引数を与えると、フィールドの[]Tもメソッドの引数もintに置き換わった型になります。

インスタンス化と型推論

型引数は呼び出しや変数宣言のときに与えます。関数呼び出しでは、引数の型から型パラメータを推論できることがあります。

n := Min(3, 5)     // Tはintと推論
var s Stack[int]

推論できない場合はMin[int](3, 5)のように明示します。インターフェースの動的型付けとは異なり、型引数はコンパイル時に決まり、型引数ごとのコードが展開されます。

type MyInt intのように型定義した型も、cmp.Orderedなどの制約を満たせば同じ型パラメータで使えます。

Min(3, 5)               // Tはint
Min(MyInt(3), MyInt(7)) // TはMyInt

式の概要

式は値を計算します。演算子とオペランドの組み合わせに加え、呼び出しや型変換なども式になります。

オペランドと演算子

オペランドは、演算の対象になる値や式です。演算子は、オペランドを組み合わせて別の値を得る記号やキーワードです。Goの演算子は、オペランドの数によって二項演算子と単項演算子に分かれます。呼び出し(len("go"))や型変換(float64(n))も式ですが、演算子の分類には含まれません。

二項演算子

二項演算子は、左と右の2つのオペランドの間に書きます。この記事後半の算術(+)、比較(==)、論理(&&)が二項演算子です。

a, b := 1, 2
sum := a + b // 二項: aとbの間の+ → 3
ok := a < b  // 二項: aとbの間の< → true

単項演算子

単項演算子は、オペランド1つの前に書きます(Goでは前置のみ)。+-は符号、!は論理否定、^はビット反転、*はポインタの間接参照、&はアドレス取得、<-はチャネル受信です。^は二項ではXORです。++--は文としてのみ使えるため、単項演算子には含まれません。

on := true
stopped := !on // 単項: onの前の! → false

var n int = 10
p := &n  // 単項: &n
v := *p  // 単項: *p → 10

算術演算子

+ - * / %とビット演算& | ^ << >> &^があります。整数除算は切り捨てです。

a, b := 10, 3
sum := a + b         // 13
diff := a - b        // 7
prod := a * b        // 30
quot := a / b        // 3(切り捨て)
rem := a % b         // 1

x, y := uint8(0b1100), uint8(0b1010)
bitAnd := x & y      // 8(0b1000)
bitOr := x | y       // 14(0b1110)
bitXor := x ^ y      // 6(0b0110)
shiftLeft := x << 1  // 24
shiftRight := x >> 1 // 6
bitAndNot := x &^ y  // 4(0b0100)

比較演算子

== != < <= > >=は比較可能なオペランドに使えます。スライス、マップ、関数は互いに==できません(スライスはnilとの比較だけ可能)。配列は要素が比較可能なら==できますが、スライスは長さと配列への参照を持つ型であり、要素ごとの等価は言語が定義していません。ポインタの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

論理演算子

&& || !はブール値に使います。&&||は短絡評価されます。

&&は左のオペランドがfalseなら、結果はfalseと決まるため右を評価しません。||は左がtrueなら、結果はtrueと決まるため右を評価しません。右側に関数呼び出しなど副作用のある式を書いたとき、実際に評価されるかはこの規則で決まります。!!activeのように直後の真偽値を反転します。

func sideEffect(n int) int { return n }

active := true     // true
paused := !active  // false

if false && sideEffect(2) == 2 { // → false(sideEffect は呼ばれない)
    // 左が false のため sideEffect は呼ばれない
}

if true || sideEffect(2) == 2 { // → true(sideEffect は呼ばれない)
    // 左が true のため sideEffect は呼ばれない
}

++--

++--は文としてのみ使え、式の中では使えません。i++は単独の文としてのみ有効で、f(i++)のように式のオペランドにはできません。

i := 0
i++ // i--
// f(i++)  // コンパイルエラー

型変換

異なる型の値を混ぜるときは、毎回明示的に変換します。Goには暗黙の数値変換がなく、intfloat64をそのまま足すとコンパイルエラーになります。

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

定数式の中では、表現可能な範囲でより広い型に暗黙的に解釈される場合があります。変数どうしの演算では暗黙変換は起きません。

const c = 42
var f float64 = c // OK(無型定数42はfloat64に表現可能)

var i int = 42
// var g float64 = i  // コンパイルエラー: 変数は暗黙変換されない
var g float64 = float64(i)

var x int = 1
var y float64 = 2
// z := x + y  // コンパイルエラー: 型が違う変数同士は演算できない

truthy/falsyがない

数値や文字列、nil相当の値が条件式で自動的に真または偽として扱われる考え方(truthy / falsy)がありますが、Goはこれを採用していません。

ifforの条件、&& || !のオペランドはbool型の式に限られます。数値どうしと同様、他型からboolへの暗黙変換はありません。bool(n)のような明示的な変換も定義されていません。変数どうしは型を合わせる必要がありますが、intstringからboolへは変換できないため、比較式で真偽を書きます。

n := 1
s := "go"
var err error

// if n { }        // コンパイルエラー: 条件はbool型
// 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

演算子の優先順位

複数の演算子が並ぶ式では、結びつきの強さでどの部分が先にまとめられるかが決まります。

二項演算子

二項演算子は左右2つのオペランドの間に書きます。仕様では優先度1(弱い)から5(強い)が付けられています。

優先度演算子
5* / % << >> & &^
4+ - | ^
3== != < <= > >=
2&&
1||

同じ優先度の二項演算子は左から右に結びつきます。42 + a - b(42 + a) - bと同じ意味です。

n := 1 + 2*3 // 2*3 が先 → 7

単項演算子

単項演算子はオペランド1つの前に書き、どの二項演算子よりも強く結びつきます。+ - ! ^ * & <-が対象です。^は単項ではビット反転、二項ではXORです。++--は文としてのみ使えるため、優先順位の表には含まれません。

評価順序

式を評価するとき、まとまったオペランドは一般に左から右に評価されます。一方、関数呼び出しの複数の引数をどの順で評価するかは仕様で定められていません。引数の式が共有状態を更新するような副作用を持つとき、評価順によって結果が変わります。

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か1になりうる

セレクタとインデックス

式の結果から、.または[ ]を使って一部を取り出します。

セレクタ

値.名前の形です。構造体のフィールドやメソッドにアクセスします。

type Item struct{ Name string }

func (Item) Tag() string { return "item" }

it := Item{Name: "a"}
field := it.Name // セレクタ: フィールド Name → "a"
label := it.Tag() // セレクタ: メソッド Tag → "item"

インデックス

値[添字]の形です。配列、スライス、マップ、文字列から要素を選びます。

nums := [2]int{10, 20}
fromArray := nums[0] // 配列 → 10

xs := []int{10, 20}
fromSlice := xs[1] // スライス → 20

m := map[string]int{"x": 1}
fromMap := m["x"] // マップ → 1

word := "go"
fromString := word[0] // 文字列 → 'g'(byte)

関数呼び出し

関数やメソッドを呼ぶ式です。結果は戻り値の型になります。

import "strconv"

result := strconv.Itoa(42) // "42"
制御構文

if

ifは条件式がtrueのとき、続くブロックを実行します。else ifelseで別の条件やそれ以外の処理を続けられます。

if x < 0 {
    return -x
} else if x == 0 {
    return 0
}
return x

ifキーワードのあと、条件式の前に;で区切った初期化文を書けます。初期化文で宣言した変数のスコープは、そのif文全体(各else if / elseを含む)に及びます。ブロックの外では使えず、関数全体で使えるわけではありません。

if n, err := strconv.Atoi(s); err != nil {
    return err
} else {
    fmt.Println(n) // n はここまで有効
}
// fmt.Println(n) // コンパイルエラー

エラー処理では、成功パスに進む前にerrをチェックします。ifの本体がreturnbreakで終わるとき、不要なelseを省略するのが慣習です。

f, err := os.Open(name)
if err != nil {
    return err
}
codeUsing(f)

for

Goにはwhileがありません。ループはforのみです。書き方は次の4通りです。

初期化・条件・後処理

初期化文、条件式、後処理を;で区切って並べます。3つそろえた基本形です。

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

条件のみ

初期化と後処理を省略し、条件式だけを書きます。条件がtrueの間、本体を繰り返します。

for n < 100 {
    n *= 2
}

条件なし

条件式を書かないと、本体を終了しない限り繰り返します。breakreturnで抜けます。

for {
    if done {
        break
    }
}

range

配列、スライス、文字列、マップ、チャネルを走査します。2変数形式では添字(またはキー)と値を受け取ります。マップの走査順は不定です。

for i, v := range slice {
    fmt.Println(i, v)
}

整数nに対するfor i := range n(Go 1.22以降)は、0からn-1までを順に取り出します。

for i := range 3 {
    fmt.Println(i) // 0 1 2
}

breakとcontinue

breakは最も内側のforswitchselectを抜けます。continueは最も内側のforの次の反復へ進みます。

for i := 0; i < 5; i++ {
    if i%2 == 0 {
        continue
    }
    if i > 3 {
        break
    }
    fmt.Println(i) // 1 3
}

switch

switchは式の値に応じて、一致したcaseの本体を実行します。どのcaseにも当てはまらないときはdefaultが実行されます。

switch day {
case "Mon":
    fmt.Println("月曜")
case "Tue", "Wed":
    fmt.Println("火〜水")
default:
    fmt.Println("その他")
}

1つのcaseにカンマ区切りで複数の値を並べると、いずれかに一致したときその本体が選ばれます。

caseの本体を実行したあと、switchは自動的に抜けます。次のcaseへ処理を続けたいときだけ、本体の末尾にfallthroughを書きます。fallthroughは直後のcaseの本体も実行し、そのcaseでもfallthroughがなければそこでswitchを抜けます。

switch rank {
case 1:
    fmt.Println("金")
    fallthrough
case 2:
    fmt.Println("銀")
case 3:
    fmt.Println("銅")
}
// rank=1 → 金, 銀(fallthrough で case 2 も実行)
// rank=2 → 銀
// rank=3 → 銅

式を書かないswitchは、各caseの条件式を上から順に評価する分岐です。最初にtrueになったcaseの本体が実行されます。

switch {
case x < 0:
    fmt.Println("負")
case x == 0:
    fmt.Println("ゼロ")
default:
    fmt.Println("正")
}

switchキーワードのあと、式の前に;で区切った初期化文を付けられます。宣言した変数のスコープは、その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で宣言した変数のスコープは、そのswitch文全体です(if初期化と同様)。

func describe(x any) {
    switch v := x.(type) {
    case int:
        fmt.Println("int", v)
    case string:
        fmt.Println("string", v)
    default:
        fmt.Println("other", v)
    }
}
関数とメソッド

定義と戻り値

関数はfuncキーワードで定義します。名前、引数、戻り値の型を書き、returnで値を返します。続く引数が同じ型のとき、型を1回だけ書いてa, b intと省略できます。a int, b intと書いても同じ意味です。型が混ざるときは同型が連続する部分だけ省略し、それ以外はそれぞれ型を書きます。

func add(a, b int) int { // a int, b intと同じ
    return a + b
}

func mix(a, b int, factor float64) float64 { // a int, b int, factor float64
    return float64(a+b) * factor
}
// mix(1, 2, 1.5) → 4.5

複数戻り値とerror

戻り値の型を(T1, T2)のように並べると、複数の値を返せます。呼び出し側はv, err := f()のように左辺の変数の数だけ受け取ります。

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登録時に評価され、関数終了時ではありません。ループ内でdeferを書くと、登録は繰り返されますが実行は関数終了時まで遅延します。

for i := 0; i < 3; i++ {
    defer fmt.Println(i) // 登録時のiをキャプチャ → 終了時に2, 1, 0の順
}

if初期化文との組み合わせ

呼び出しとerrのチェックを1行にまとめるときにif初期化文を使います。

if _, err := strconv.Atoi(input); err != nil {
    return err // inputが整数でなければ終了
}

名前付き戻り値

戻り値の型の前に名前を付けると、その名前が関数内の変数として使えます。

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return // この時点のx, yを返す(return x, yと同じ)
}

戻り値に名前を付けているとき、値を書かないreturnはnaked returnと呼ばれ、名前付き戻り値をそのまま返します。名前付き戻り値はドキュメントとして有用ですが、長い関数では何を返しているか追いにくくなります。defer内で名前付き戻り値を書き換えるとreturnの結果が変わるため、意図しない場合があります。

可変長引数

末尾の引数を...Tと書くと可変長引数になります。呼び出し側は値を列挙するか、スライスを...付きで渡します。

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)
sum([]int{4, 5}...)

値渡しと関数値

関数は第一級の値です。引数は値のコピーであり、呼び出し側の変数は関数内で引数を書き換えても変わりません。関数型の変数に代入して、別名から呼び出せます。

func double(n int) int {
    m := n
    n = 0 // コピーnを書き換えても呼び出し側には及ばない
    return m * 2
}

x := 10
y := double(x) // yは20
// xは10のまま

var fn func(int) int = double
z := fn(3) // zは6

関数リテラル

funcに名前を付けず書いた式が関数リテラル(無名関数)です。第一級の値なので、変数に代入してから呼び出せます。

add := func(x, y int) int {
    return x + y
}
sum := add(1, 2) // 3

即時呼び出し

関数リテラルの直後に(引数…)を付けると、変数に代入せずその場で呼び出せます。

n := func(x int) int { return x * 2 }(3) // 6

クロージャ

関数リテラルは外側の関数で宣言した変数を参照できます。参照した変数はリテラルと共有され、リテラルが参照可能な間は保持されます。

func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

next := counter()
v1 := next() // 1
v2 := next() // 2

func (c Counter) Inc()と値レシーバで書くと、呼び出し元のc.nは増えません。メソッドは型に紐づく関数で、値レシーバとポインタレシーバでメソッド集合が変わり、インターフェース実装の可否にも影響します。

メソッド宣言

メソッドは型に紐づく関数です。レシーバは関数名の前に書きます。

type Rect struct{ W, H float64 }

func (r Rect) Area() float64 {
    return r.W * r.H
}

レシーバ名は型の短い別名として使われます。同じ型のメソッド集合内で一意である必要があります。

値レシーバとポインタレシーバ

レシーバは値Tまたはポインタ*Tです。

func (r Rect) Perimeter() float64 {
    return 2 * (r.W + r.H)
}

func (r *Rect) Scale(f float64) {
    r.W *= f
    r.H *= f
}

値レシーバではメソッド内の変更はコピーに対してのみ有効です。呼び出し元の値を変更するにはポインタレシーバを使います。

代入可能な値へのアドレス取得

変数pが代入可能なら、値型のレシーバメソッド呼び出しp.Method()は、コンパイラによって(&p).Method()と解釈されることがあります。

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) // 代入可能 → 実質 (*Rect).Scale
fmt.Println(r.Area()) // 24

一方、リテラルやmapの要素など代入可能でない値に対して、ポインタレシーバのメソッドは直接呼べません。

// Rect{W: 1, H: 1}.Scale(2) // コンパイルエラー

ポインタと値の使い分け

次のようなときはポインタレシーバが使われます。

  • メソッドがレシーバのフィールドを変更する
  • レシーバが大きく、コピーを避けたい
  • レシーバがスライスやマップなど、内部データを変更する型である

値レシーバは小さな不変型や、値型として扱いたいときに向きます。大きな構造体では値レシーバのたびに全体がコピーされるため、読み取り専用でもサイズが大きければポインタレシーバを検討します。

ポインタレシーバ*Tのメソッドは、レシーバがnilでも呼び出し自体は可能です。メソッド内でフィールドにアクセスするとパニックになります。値レシーバのメソッドをnil*Tから呼ぶと(*p)に展開されるため、フィールド参照時にパニックします。

var p *Rect
// p.Area()   // 値レシーバ: (*p) の展開で nil ならパニック
// p.Scale(2) // ポインタレシーバ: 呼び出しは可能だが、フィールド参照でパニック

レシーバ型の一貫性

同一の型に対して、値レシーバとポインタレシーバのメソッドを混在させないのが慣習です。混在するとメソッド集合が呼び出し側の型(T*Tか)によって変わり、インターフェース実装の可否にも影響します。値型Tのメソッド集合には値レシーバのメソッドが入り、ポインタ型*Tには値レシーバとポインタレシーバの両方が入ります。

メソッド式とメソッド値

メソッドは関数値として扱えます。

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 が昇格

外側の型に同名メソッドがある場合、外側が優先されます。インターフェースの埋め込みはメソッド集合の合成であり、構造体の埋め込みとは別の仕組みです。埋め込みはフィールドとメソッドの昇格であり、型の置き換え(代入可能性)を生みません。CarEngineとして使うことはできません。

概要

組み込み関数は事前宣言されており、インポートなしで使えます。スライス・マップ・チャネルの確保や長さ操作など、言語の基本操作を担います。

newとmake

new(T)は任意の型Tのゼロ値を置いた領域を確保し、そのアドレス*Tを返します。メモリの置き場には、関数の呼び出しに合わせて使うスタックと、関数をまたいで値を置けるヒープがあります。ヒープ上の不要な領域はガベージコレクションが回収します。どちらに置くかはGoが決め、プログラマが指定する必要はありません。newはヒープ専用の確保ではありません。

p := new(int) // *int、*p == 0

makeはスライス・マップ・チャネルだけを対象に、内部構造を初期化したTそのものを返します。ポインタではありません。これらの型は内部構造の初期化が必要なためnewではなくmakeを使います。new([]int)nilスライスへのポインタを返すだけで、要素を足せる状態にはしません。

s := make([]int, 0, 10) // len=0, cap=10
m := make(map[string]int)
ch := make(chan int, 1)

lenとcap

lenは配列、スライス、マップ、文字列、チャネルの長さ(またはバッファ内の要素数)を返します。スライスと配列ではcapが容量を返します。

s := make([]int, 2, 5)
fmt.Println(len(s), cap(s)) // 2 5

m := map[string]int{"a": 1}
fmt.Println(len(m)) // 1

appendとcopy

appendは新しいスライスヘッダを返します。cap不足時は新しい元配列が割り当てられることがあります。戻り値を呼び出し元の変数へ代入しないと、呼び出し元から見た長さは変わりません。再割り当て後も古いヘッダを使い続けると、別の配列を指している可能性があります。

s := []int{1}
s = append(s, 2, 3)
s = append(s, []int{4, 5}...)

copy(dst, src)dstへ要素をコピーし、実際にコピーした個数を返します。コピー数は両者の短い方の長さまでです。

dst := make([]int, 2)
n := copy(dst, []int{10, 20, 30})
fmt.Println(n, dst) // 2 [10 20]

deleteとclear

delete(m, key)はマップからキーを削除します。キーが無くてもパニックにはなりません。

m := map[string]int{"a": 1}
delete(m, "a")
delete(m, "missing") // 安全

clear(Go 1.21以降)は型によって動きが違います。マップではすべてのエントリを削除して空にします。スライスでは各要素をゼロ値にし、長さlenはそのままです。

m := map[string]int{"a": 1, "b": 2}
clear(m)
fmt.Println(len(m)) // 0

s := []int{1, 2, 3}
clear(s)
fmt.Println(s, len(s)) // [0 0 0] 3

close

close(ch)はチャネルを閉じ、「もう送らない」ことを示します。送信側が呼ぶのが慣習で、rangeの終了条件にもなります。閉じたチャネルへの送信はパニックです。

ch := make(chan int, 1)
ch <- 1
close(ch)
for v := range ch {
    fmt.Println(v) // 1
}

minとmax

minmax(Go 1.21以降)は、比較可能な同じ型の引数のうち最小・最大を返します。

fmt.Println(min(3, 1, 2)) // 1
fmt.Println(max(3, 1, 2)) // 3

panicとrecover

panicは実行を中断し、recoverdefer内でその中断から復帰します。想定できる失敗は通常errorで返し、panicはプログラムのバグや回復不能な状態向けです。

エラーとpanic

error型

errorは組み込みインターフェースです。

type error interface {
    Error() string
}

エラーは値

Goでは失敗を例外で飛ばさず、通常は戻り値のerrorとして返します。呼び出し側は毎回if err != nilで分岐し、成功パスと失敗パスを明示的に書きます。

これは制御フローの一部です。エラーを無視するには_で捨てる必要があり、コンパイラは未使用のerrorを警告しませんが、チェックを省略すると失敗が伝播しません。errorを無視して後続の値(nilポインタ、ゼロ値など)を使い続けると、別の場所でパニックやデータ破損につながることがあります。

f, _ := os.Open("missing.txt")
var buf [10]byte
_, _ = f.Read(buf) // 開け失敗時fはnil → panic

呼び出し側は取得直後にerrをチェックします。

f, err := os.Open("file.txt")
if err != nil {
    return err
}
// ここに来た時点でfは開けている

エラーの生成

エラー値はerrors.Newfmt.Errorfで作ります。

固定の文言だけで足りるときはerrors.Newを使います。

return errors.New("not found")

パスや件数など、値をメッセージに埋め込むとき、または下位のerrorに文脈を足して返すときはfmt.Errorfを使います。下位のerrorを包むときは%wを使います。単に文字として並べるだけなら%vです。

return fmt.Errorf("open %s: %w", path, err) // errを包む(呼び出し側で取り出せる)

return fmt.Errorf("open %s: %v", path, err) // errを文字列として埋め込むだけ

return fmt.Errorf("%d items left", n)

呼び出し側では、errors.Isで元のエラーかどうかを調べられます。%wで包んだときだけ一致し、%vではメッセージに文字が入るだけで元のerrorとしては取り出せません。

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

nil errorは成功

errorのゼロ値はnilです。err == nilのとき処理は成功したとみなします。非nilのerror値が返ったとき、呼び出し側は失敗として扱います。

func mayFail(ok bool) error {
    if ok {
        return nil // 成功
    }
    return errors.New("failed")
}

if err := mayFail(true); err != nil {
    // 失敗
} else {
    // err == nilなら成功
}

if err != nilの位置

エラーは直後にチェックするのが慣習です。後続のコードが成功を前提に書けるようになります。

panic

panicは実行時に異常を報告し、通常はそのゴルーチンの実行を中断します。

ライブラリの公開APIでは、呼び出し側が予期しうる失敗(ファイルが無い、形式が不正など)はerrorで返します。panicはプログラムのバグ、不変条件の破れ、回復不能な内部状態向けです。予期可能な失敗をpanicにすると、呼び出し側が通常のerrorチェックで扱えず、APIの契約が一貫しなくなります。

if p == nil {
    panic("nil pointer") // バグの検出向け
}

recover

recoverpanicによって中断された実行を再開します。deferされた関数の中でのみ有効です。recoverは最後の防衛線であり、ループや分岐の代わりに使うと読みにくく、想定外のpanicも飲み込んでしまいます。

defer func() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}()

recoverのスコープ

recoverが効くのは、同じゴルーチン内でpanicしたときだけです。別ゴルーチンで起きたpanicは、そのゴルーチンを終了させ、回復されなければプログラム全体が終了します。呼び出し元のdefer内のrecoverは、別ゴルーチンで起きたpanicを捕まえられません。

並行処理

ゴルーチンとOSスレッド

goを付けた呼び出しは、元の処理と並行して走ります。ゴルーチンはGoランタイムが管理する実行単位です。OSスレッドより起動コストが小さく、多数を同時に動かせます。ランタイムは複数のゴルーチンを少数のOSスレッドに割り当てて実行します。

構文はgoのあとに関数呼び出しを書きます。呼び出し先の処理が終わるのを待たず、go文の次の文がすぐ実行されます。

go func() {
    fmt.Println("別のゴルーチンから")
}()
fmt.Println("main側")

main終了とゴルーチンの寿命

mainゴルーチンがreturnするとプログラム全体が終了し、他のゴルーチンも打ち切られます。mainが先に終わると、起動したばかりの処理の完了を待たずに終了します。

go fmt.Println("worker")
// mainがすぐ終わると、workerの出力が欠けることがある

完了を待つには、チャネルやsync.WaitGroupなどの同期が必要です。

送信と受信

チャネルはゴルーチン間で値を渡す型です。要素型Tのチャネルはchan Tと書きます。

ch <- vはチャネルchvを送り、<-chchから値を受け取ります。<-の左側がチャネルです。

受信は式にも文にも書けます。x := <-chのように値を受け取るほか、<-chだけで値を捨てることもできます。

ch := make(chan string)
go func() {
    ch <- "ping" // 送信
}()
msg := <-ch // 受信
fmt.Println(msg) // ping
ch <- v    // 送信
x := <-ch  // 受信
<-ch       // 値を捨てる受信

バッファなしチャネルの待ち合わせ

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が使えます。Addで待つ数を登録し、各ゴルーチンの終了時にDone、待ち側で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() // 両方のDoneが終わるまで待つ

Addはゴルーチン起動より前に呼びます。起動後にAddすると、カウンタがまだ増える前にWaitが「待ちは終わった」と判断して先に進んでしまうことがあります。

バッファ付きチャネル

make(chan int, 3)は容量3のバッファ付きチャネルです。バッファに空きがある間は、受信側を待たずに送信できます。バッファが満杯なら送信はブロックし、空なら受信はブロックします。

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)は「これ以上値を送らない」というシグナルです。送信側が呼ぶのが慣習です。受信側が閉じると役割が壊れ、そのあと送信側が送り続けると閉じたチャネルへの送信でパニックになります。

閉じたチャネルへの送信

閉じたチャネルへ送ると、実行時にパニックします。

ch := make(chan int)
close(ch)
// ch <- 1 // panic: send on closed channel

閉じたチャネルからの受信

残りの値を取り出したあと、ゼロ値と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(閉じ済み)

rangeとclose

range chはチャネルが閉じられるまで受信を続けます。送信側がcloseしないと、rangeは終わりません。複数の値を送り終えたあと、完了の合図としてもcloseが使えます。

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がある前提で書きます。

他のゴルーチンから見えるとは

Goのメモリモデルは、他のゴルーチンの書き込みが、いつ自分の読み取りから見えるかを定めます。次の例では、別ゴルーチンがreadyに書いたあとでも、読み取り側でtrueとは限りません。

var ready bool
go func() {
    ready = true
}()
_ = ready // readyがtrueとは限らない

ソースに書いた文の順どおりに、他のゴルーチンからも見えるとは限りません。コンパイラやCPUは速度のために、1つのゴルーチン内の読み書きの実行順を入れ替えることがあり(並べ替え)、書き込みの一部だけが先に観測されることがあります。

次の例では、書き込み側はaのあとにbを更新しています。それでも読み取り側が先にb == 2を見ても、その時点でa == 1とは限りません。2のあとに0と表示されることがあります。

var a, b int

go func() {
    a = 1
    b = 2
}()

fmt.Print(b)
fmt.Print(a) // 「20」や「00」などになりうる。「21」とは限らない

書き込みを確実に見せるには、チャネルの送受信やsync.WaitGroupで相手の完了を待つか、sync.Mutexで同じ変数へのアクセスを排他します。こうした、ゴルーチン同士の順序や排他をランタイムが保証する操作を同期と呼びます。

データ競合

2つ以上のゴルーチンが、同期を挟まずに同じ変数へアクセスし、少なくとも1つが書き込みであるとき、データ競合が起きます。競合するアクセスの結果は保証されません。boolへの1回の代入でも、他ゴルーチンからの可視性は保証されません。

var n int
go func() { n = 1 }()
fmt.Println(n) // 0とも1とも限らない(データ競合)

go test -raceで検出できます。検出器は実行したパスに依存するため、通らなかった経路の競合は残る可能性があります。

チャネルで見えるようにする

バッファの有無を問わず、チャネルへの送信は、対応する受信の完了より先に起きたとみなされます。送信より前の書き込みは、受信が終わったあとの読み取りから見えます。

var ready bool
ch := make(chan struct{})

go func() {
    ready = true
    ch <- struct{}{}
}()

<-ch
// この時点でready == trueが見える

値を送る代わりにclose(ch)しても同じです。閉じる前に書いた内容は、相手が受信で「閉じた」と分かったあとに見えます。

var ready bool
ch := make(chan struct{})

go func() {
    ready = true
    close(ch) // 送信の代わりに閉じる
}()

<-ch // 閉じたチャネルからの受信
// この時点でもready == trueが見える

チャネルを使わずreadyだけを読むと、上の保証は付きません。

バッファ付きチャネルでは、送信が受信を待たずに進むことがあります。その場合でも、「対応する受信が完了したあと」であれば、その送信より前の書き込みは受信側から見えます。

逆に、受信する側が書いた値を、送信が終わっただけで読もうとすると、バッファ付きでは見え方が保証されません。次の例では別ゴルーチンが先に受信し、mainが送信しています。バッファに空きがあるためch <- struct{}{}は相手の受信を待たずに進められ、その直後のreadyの読み取りでtrueとは限りません。

var ready bool
ch := make(chan struct{}, 1) // バッファ付き

go func() {
    ready = true
    <-ch // 受信側の書き込みのあとで受信
}()

ch <- struct{}{}
_ = ready // readyがtrueとは限らない(バッファ付きのため)

バッファなし(make(chan struct{}))なら、受信が先に待っていても送信が後から来ても、送受信は対になって同期します。受信が完了した時点で、対応する送信より前の書き込み(ここではready = true)が見えます。

Mutexで同じ変数を守る

複数のゴルーチンが同じ変数を同時に読み書きすると、データ競合になります。同時に触らせないようにする道具がsync.Mutexです。

Mutexは通行証1枚だと考えます。Lockに成功したゴルーチンだけが通行証を持ち、そのあいだに共有変数を更新します。他のゴルーチンが同じMutexLockすると、前のゴルーチンがUnlockするまで待ちます。

var (
    mu sync.Mutex
    n  int
)

mu.Lock()
n++
mu.Unlock()

守りたい変数へ触るすべての経路で、同じmuを先にLockします。Lockせずにnを触るゴルーチンがいれば、そのアクセスは待ち行列に入らず、排他になりません。muは「どの変数を守るか」を覚えておらず、同じ通行証を使う処理どうしが順番待ちになる、という仕組みです。

データとロックをまとめて扱うときは、構造体のフィールドに置くことも多いです。置き場所が変わっても手順は同じで、t.nに触る前にt.mu.Lockします。

type Tally struct {
    mu sync.Mutex
    n  int
}

func (t *Tally) Inc() {
    t.mu.Lock()
    t.n++
    t.mu.Unlock()
}

func (t *Tally) Value() int {
    t.mu.Lock()
    defer t.mu.Unlock()
    return t.n
}

あるゴルーチンのUnlockのあと、別ゴルーチンが同じミューテックスでLockに成功した時点から、そのUnlockより前の書き込みが見えます。Incで増やしたnは、後続のValueから読めます。

LockUnlockのペアを崩すと、排他も可視性の保証も壊れます。Unlockを忘れないよう、関数を抜ける経路が複数あるときはdefer t.mu.Unlock()が使われます。

モジュールとパッケージ

モジュールとgo.mod

モジュールは、ルートにgo.modを置いたソースの木です。依存の境界であり、go.modmodule行がモジュールパスになります。そのモジュール内のパッケージのimportパスは、このモジュールパスを先頭にします。

mkdir demo
cd demo
go mod init example.com/demo
module example.com/demo

go 1.26

go行は、このモジュールが想定する言語の版です。サブディレクトリごとにgo mod initする必要はありません。同じgo.modの下にあるパッケージは、すべてこのモジュールに属します。

外部パッケージをimportすると、必要なモジュール版がgo.modに記録されます。依存の整理にはgo mod tidyが使われます。

パッケージ

パッケージはコンパイルの単位であり、識別子の名前空間でもあります。通常、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"))
}

greeterディレクトリのimportパスは、モジュールパスに相対ディレクトリを足したexample.com/demo/greeterです。

importパスとパッケージ名

importパスは、import "..."に書く文字列です。モジュールパスにディレクトリを足したもので、「どのフォルダのコードを取り込むか」を指定します。

パッケージ名は、取り込んだあとにコード上で使う名前です。そのディレクトリの.goファイル先頭のpackage句で決まります。importパスの末尾と同じとは限りません。

揃っている例です。ディレクトリgreeter/のソースがpackage greeterなら、呼び出しもgreeterで書きます。

// greeter/greet.go
package greeter

func Hello(name string) string {
    return "hello, " + name
}
import "example.com/demo/greeter" // importパス(場所)

greeter.Hello("Go") // パッケージ名(package句)

package句だけ違うと、importはできても呼び出し名が変わります。たとえばpackage greetならgreet.Helloであり、greeter.Helloは見つかりません。

// greeter/greet.goがpackage greetのとき
import "example.com/demo/greeter"

greet.Hello("Go") // 使えるのはpackage句の名前
// greeter.Hello("Go") // コンパイルエラー

パッケージ名は短い小文字の単語1つが望ましいです(httpjsonbufioなど)。呼び出し側は常にパッケージ名.識別子と書くため、型名や関数名にパッケージ名を重ねると冗長になります。たとえばバッファ付きの読み取り型はbufio.Readerであり、bufio.BufReaderのようにはしません。

公開と非公開

識別子の公開範囲は、名前の先頭文字で決まります。大文字で始まる名前は他パッケージから参照でき、小文字で始まる名前は同じパッケージ内だけです。

package greeter

func Hello() string { // 他パッケージから呼べる
    return hello()
}

func hello() string { // 同じパッケージ内だけ
    return "hello"
}
import "example.com/demo/greeter"

greeter.Hello()
// greeter.hello() // コンパイルエラー: 小文字は他パッケージから見えない

import宣言

ソースはpackage句のあと、import、その後に宣言が続きます。

import "fmt"

import (
    "errors"
    myio "io"
    _ "image/png"
)

別名を付けると、パッケージ名の衝突を避けられます。myio "io"ならmyio.Readerのように使います。

識別子を使わず、パッケージの初期化だけ実行したいときは_ "image/png"のようにブランクimportします。デコーダの登録など、initの副作用が目的のときに使います。

import . "fmt"は、パッケージ名なしで識別子を取り込む書き方です。通常ならfmt.Printlnと書くところを、Printlnだけで呼べます。

import . "fmt"

func main() {
    Println("hello") // fmt.Println("hello")と同じ
}

Printlnが自パッケージの関数なのかfmtのものなのか、呼び出し箇所だけでは分かりにくくなります。そのため通常のコードでは使いません。

internalパッケージ

パスにinternalを含むパッケージは、そのinternalの親ディレクトリ以下からだけimportできます。モジュール全体から見えるわけではなく、ディレクトリの位置で決まります。

たとえばexample.com/demo/internal/storeは、example.com/demo/...配下からはimportできますが、別モジュールやexample.com/demoの外にあたるパスからはimportできません。公開したくない実装を内側に閉じるための仕組みです。

ドキュメントコメント

公開する宣言の直前に書いたコメントが、ドキュメントとして扱われます。

// ParseConfigは設定ファイルを読み込む。
func ParseConfig(path string) (*Config, error) {
    // ...
}

フォーマット

Goのソースは、手で細かく揃えるのではなくgofmt(パッケージ単位ならgo fmt)で機械的に整形するのが慣習です。インデントはタブ、空白の入れ方や並びも標準形に揃います。スタイルの好みで議論せず、ツールの出力に合わせます。

ファイルをその場で書き換える例です。

gofmt -w main.go

パッケージ(ディレクトリ)単位で揃えるときはgo fmtを使います。結果のスタイルはgofmtと同じです。

go fmt ./...

gofmtは標準入力の断片も整形できます。go fmtはパスで指定したパッケージ配下の.goファイルをまとめて扱います。

テスト

テストは、対象と同じディレクトリに_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が必要で、大文字の公開APIだけを試せます。利用側と同じ見え方で公開面を確認したいときに使います。非公開の識別子はここからは触れません。

// 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() // コンパイルエラー: 他パッケージからは見えない
}

どちらもよく使われます。非公開まで含めて試すなら同じパッケージ、公開APIだけを外から試すならgreeter_test、という分け方になります。1つのディレクトリに両方を置くこともできます。

go getで追加し、コードから使う

go getは、指定したモジュールを現在のモジュールの依存として足し、ソースをモジュールキャッシュへ取得します。モジュールキャッシュは、ダウンロードしたモジュールのソースをマシン上に蓄える場所です。既定では$GOPATH/pkg/modにあり、プロジェクトをまたいで再利用されます。プロジェクト直下にライブラリ用のディレクトリは増えません。依存の記録は、プロジェクト直下のgo.modに残ります。

go get rsc.io/quote

go getだけでは、そのパッケージの関数はまだ呼べません。ソースにimportを書き、識別子を使います。

package main

import (
    "fmt"

    "rsc.io/quote"
)

func main() {
    fmt.Println(quote.Hello())
}
go run .

fmtのような標準ライブラリはgo getの対象ではありません。Goのインストールに含まれており、go.modrequireにも現れません。

go.modとgo.sum

依存を足すと、go.modrequire行が増えます。使うモジュールとバージョンの記録です。ここに書くバージョンは「このバージョンに固定」ではなく「このバージョン以上」です。自分のgo.modexample.com/lib v1.2.0とあっても、使っている別モジュールのgo.modに同じモジュールがv1.4.0とあれば、ビルドではv1.4.0が使われます。

module example.com/demo

go 1.26

require rsc.io/quote v1.5.2

あわせてgo.sumができます(または更新されます)。ここには、取得したモジュールバージョンの内容のチェックサムが並びます。次回以降のダウンロードで、同じバージョンの中身が変わっていないかを確かめます。

go.modは「何をどのバージョン以上で使うか」、go.sumは「そのバージョンの中身の指紋」です。役割が違うため、両方をソースと同じバージョン管理に含めます。

requireの行末に// indirectと付くことがあります。メインモジュールのパッケージから直接importされていないモジュールの印です。たとえばrsc.io/quoteを足すと、quoteが使うrsc.io/samplerなどがこの印付きで並ぶことがあります。go getした時点でまだimportしていなければ、そのモジュール自身にも同じ印が付きます。自分のコードへimportを足して直接使うようになれば、この印は外れます。手で付け外しする必要はなく、go getgo mod tidyが更新します。

go mod tidyと依存のやめ方

もう一つの正規の足し方は、先にソースへimportを書き、あとから依存を揃えることです。

import "rsc.io/quote"
go mod tidy

go mod tidyは、モジュール内のソースがimportしている集合に合わせて、足りないモジュールをgo.modへ足し、どのパッケージからも使われないモジュールを落とします。go.sumも同様に揃えます。

依存をやめるときも、まずソースからそのimportと使用箇所を消し、もう一度go mod tidyを実行します。参照が無ければ、対応するrequirego.modから外れます。

バージョンの指定

バージョンを明示するときは、モジュールパスのあとに@とバージョンを付けます。

go get rsc.io/[email protected]
go get rsc.io/quote@latest

@latestは、利用できるなかで最も高いリリースバージョンを選ぶ指定です。リリースバージョンがなければプレリリース、タグがなければリポジトリ既定ブランチ先端のバージョンになります。バージョンを省略したgo get rsc.io/quoteも、取得時点で選ばれた具体的なバージョンがrequireに書かれます。go.modrequirelatestという文字列を手で書いてはいけません。バージョンを変えるときはgo getを使います。

メジャーバージョンとimportパス

メジャーバージョンが2以上のモジュールでは、モジュールパスの末尾に/vNが付きます。go getにも、importに書く文字列にも、そのパスを使います。

go get rsc.io/quote/v3@latest
import "rsc.io/quote/v3" // v3のモジュールパス

quote.HelloV3()

import "rsc.io/quote"はv1のモジュールパスです。これではv3のパッケージは使えません。

go getとgo install

現在のモジュールの依存を変えるのがgo getです。揃えるのがgo mod tidyです。

go get rsc.io/quote

一方、バージョン付きで使うgo installは、コマンドを自分の環境へ入れる操作です。実行ファイルは$GOBINがあればそこへ、なければ$GOPATH/binに入り、パスが通っていればその場で呼べます。

go install golang.org/x/tools/cmd/stringer@latest
stringer -help

このプロジェクトのgo.modrequireは増えません。依存を足したいのにgo installだけ実行すると、プロジェクト側の記録は変わらないままになります。