基本型
数値型
整数型には符号付きのint8、int16、int32、int64、int、符号なしのuint8、uint16、uint32、uint64、uint、uintptrがあります。日常の計数にはintを使うことが多いです。
浮動小数点は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の別名で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: 語byteとruneは独立した型名ではなく、uint8とint32の別名です。
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 // *Pointp1とp2は構造体の値そのものです。p3は複合リテラルで作った値のアドレスを、その場で取った*Pointです。pを用意してp4 := &pとする書き方も同じ意味で、p3とp4は同じ型・同じ内容のポインタになります。違いは、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ではないCarをEngineとして代入することはできません。型の関係は「中に持っている」だけです。埋め込みを継承と考えると、インターフェース実装の判定を誤解しやすくなります。実装の有無は外側型のメソッド集合で判定されます。
比較
すべてのフィールドが比較可能なときだけ==で比較できます。スライス、マップ、関数を含む構造体は比較できません(マップのキーにも使えません)。
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]midとallは別のヘッダですが、要素は同じ元配列を指します。mid側を書き換えるとallからも見えます。
2要素式の容量はcap(元) - lowです。allのcapは5、lowは1なので、midのcapは4です。
fmt.Println(len(mid), cap(mid)) // 3, 43インデックス形式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で要素だけをコピーします。dstとmidは別の元配列を指すため、dstを書き換えてもmidやallは変わりません。
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スライスと空スライス
| 式 | len | cap | == nil |
|---|---|---|---|
var s []int | 0 | 0 | true |
s := []int{} | 0 | 0 | false |
make([]int, 0) | 0 | 0 | false |
いずれもappendは可能です。
appendのルール
appendは次のように動きます。
- 現在のcapに余裕があれば、元配列に書き込み、lenを増やした新しいヘッダを返す
- 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を使います。okがfalseならキーは無く、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ならtrueとfalse、int8なら-128から127までの整数だけがint8の値です。変数に入る値は必ずこの集合のどれか1つで、型は「どんな値が存在しうるか」を決めます。
基底型と型定義
各型には基底型(underlying type)があります。事前宣言されたブール・数値・文字列型や型リテラルの基底型は、その型自身です。型定義type T Uでは、Tの基底型はUの基底型と同じになります。型エイリアスtype T = UではTとUは同一型であり、基底型も同じです。
type ID int
type Count = int // エイリアス: Countとintは同一型型定義は新しい型を作る
type Celsius float64はfloat64と別の型です。基底型が同じでも、代入には明示的な変換が必要になります。型定義は意味の違う値を混同しないための仕組みです。
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 = UはUと同一型です。
複合型は、配列・スライス・構造体・ポインタ・関数・インターフェース・マップ・チャネルです。typeで別名を付けていない無名の複合型は、種類と内部の型構成が一致すれば同一型とみなされます。配列では要素型に加えて長さも型の一部になり、構造体ではフィールドの名前・型・並びが一致している必要があります。
[]intと[]intは同一type A intとtype 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.Readerのio.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 == nilはfalseです。
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
}要求されるメソッド
DuplexはPullとPushの両方を要求します。Duplexのメソッド集合はPullerとPusherの和集合です。次のようにメソッドを直書きしたのと同じ意味です。
type Duplex interface {
Pull(p []byte) (n int, err error)
Push(p []byte) (n int, err error)
}具体型と代入
PullとPushの両方を備えた型は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でなければokがfalseになります。
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 | ゼロ値の構造体 | 不可(SinkにPullがない) |
(*Sink)(nil) | *Sink | nil | 可(*SinkにPullがある) |
(*Sink)(nil)は実体を作らず、右辺の型が*Sinkであることだけをコンパイラに伝えます。代入可能性の確認だけが目的なので、インスタンスの代わりに型付きのnilを置けます。
PipeのPullは値レシーバなので、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
}制約の書き方
制約はインターフェースとして書きます。定義した制約名は、anyやcmp.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つのどれかに当てはまる型」という和集合です。int、int64、float64のいずれかを型引数にできます。
~は基底型(underlying type)を指定します。~intは「基底型がintの型」を集合に含めます。type MyInt intもNumberを満たすので、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には暗黙の数値変換がなく、intとfloat64をそのまま足すとコンパイルエラーになります。
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はこれを採用していません。
ifやforの条件、&& || !のオペランドはbool型の式に限られます。数値どうしと同様、他型からboolへの暗黙変換はありません。bool(n)のような明示的な変換も定義されていません。変数どうしは型を合わせる必要がありますが、intやstringから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"