基本型
数値型
整数型には符号付きの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) // ループ回数や処理対象が不定になる
}型定義
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がある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であることだけをコンパイラに伝えます。
型パラメータと制約
ジェネリクス(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"