
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
整数オーバーフロー(Integer overflow)
符号なし整数値に対して、演算 +、-、*、および << は 2n を法として計算される。ここで n は符号なし整数型のビット幅である。大まかに言えば、これらの符号なし整数演算はオーバーフロー時に上位ビットを破棄し、プログラムは「ラップアラウンド」に依存してよい。
符号付き整数に対して、演算 +、-、*、/、および << は合法的にオーバーフローする可能性があり、結果の値は存在し、符号付き整数の表現、演算、およびそのオペランドによって決定論的に定義される。オーバーフローは実行時パニックを引き起こさない。コンパイラはオーバーフローが発生しないという仮定のもとでコードを最適化してはならない。たとえば、x < x + 1 が常に真であると仮定してはならない。
解説
オーバーフローってなに?
整数型には格納できる値の範囲があります。その範囲を超える計算結果になることをオーバーフローと言います。
var x uint8 = 255 // uint8 の最大値
x = x + 1 // 256 は uint8 に入らない!
fmt.Println(x) // 0(ラップアラウンドして 0 に戻る)
イメージとしては、車のオドメーターが 999999 の次に 000000 に戻るのと同じです。
符号なし整数:ラップアラウンドが保証される
符号なし整数のオーバーフローは仕様で定義された動作です。つまり、バグではなく、意図的に活用してよい挙動です。
計算は「2n で割った余り」として行われます。uint8(8ビット)なら 28 = 256 を法として計算されます。
var x uint8 = 250
x = x + 10 // (250 + 10) % 256 = 4
fmt.Println(x) // 4
var y uint8 = 3
y = y - 5 // (3 - 5) を 256 で割った余り → 254
fmt.Println(y) // 254
この性質は実際に活用される場面があります。
// ハッシュ計算:オーバーフローを気にせず加算・乗算できる
var hash uint32 = 0
for _, b := range data {
hash = hash*31 + uint32(b) // オーバーフローしても OK
}
// シーケンス番号のラップアラウンド
var seq uint16 = 65535
seq++ // 0 に戻る(TCP のシーケンス番号などで使われる考え方)
符号付き整数:オーバーフローしてもパニックしない
符号付き整数のオーバーフローも実行時パニックにはなりません。結果は2の補数表現に基づいて決定論的に決まります。
var x int8 = 127 // int8 の最大値
x = x + 1
fmt.Println(x) // -128(最大値の次は最小値に折り返す)
var y int8 = -128 // int8 の最小値
y = y - 1
fmt.Println(y) // 127(最小値の前は最大値に折り返す)
ただし、符号なし整数とは違い、符号付きのオーバーフローに意図的に依存するコードはほとんどありません。多くの場合、それはバグです。
コンパイラはオーバーフローを前提にした最適化をしない
これは C 言語との大きな違いです。
C 言語では、符号付き整数のオーバーフローは未定義動作なので、コンパイラは「オーバーフローは起きない」と仮定して最適化できます。たとえば x < x + 1 を常に true として最適化することが許されます。
Go ではそれが禁止されています。
var x int = math.MaxInt // int の最大値
if x < x+1 { // オーバーフローで x+1 は負になる → false
fmt.Println("bigger")
} else {
fmt.Println("overflow!") // こちらが実行される
}
Go のコンパイラは x < x + 1 が偽になりうることを正しく考慮しなければなりません。これにより、オーバーフローが起きたときの動作が予測可能になります。
オーバーフローを検出するには
Go はオーバーフローを自動的に検出してくれないので、必要な場合は自分でチェックする必要があります。
func addSafe(a, b int) (int, bool) {
if b > 0 && a > math.MaxInt-b {
return 0, false // オーバーフローする
}
if b < 0 && a < math.MinInt-b {
return 0, false // アンダーフローする
}
return a + b, true
}
result, ok := addSafe(math.MaxInt, 1)
if !ok {
fmt.Println("オーバーフロー!")
}
標準ライブラリの math/bits パッケージにも、オーバーフローを検出できる関数があります。
sum, carry := bits.Add64(a, b, 0)
if carry != 0 {
fmt.Println("オーバーフロー!")
}
金額計算やサイズ計算など、オーバーフローが深刻な問題につながる場面では、こうしたチェックを入れる習慣をつけておくと安全です。
おわりに
本日は、Go言語の言語仕様について解説しました。

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)


コメント