Go言語入門:言語仕様 -Vol.95-

スポンサーリンク
Go言語入門:言語仕様 -Vol.95- 用語解説
Go言語入門:言語仕様 -Vol.95-
この記事は約7分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

数値型間の変換(Conversions between numeric types)

非定数の数値の変換には、以下のルールが適用される:

  1. 整数型間の変換において、値が符号付き整数の場合は暗黙の無限精度まで符号拡張される。そうでなければゼロ拡張される。その後、結果の型のサイズに合うように切り捨てられる。たとえば、v := uint16(0x10F0) の場合、uint32(int8(v)) == 0xFFFFFFF0 となる。変換は常に有効な値を生成する。オーバーフローの通知は行われない。
  2. 浮動小数点数を整数に変換するとき、小数部分は破棄される(ゼロに向かう切り捨て)。
  3. 整数または浮動小数点数を浮動小数点型に変換するとき、または複素数を別の複素数型に変換するとき、結果の値は変換先の型で指定される精度に丸められる。たとえば、float32 型の変数 x の値は IEEE 754 の32ビット数の精度を超える追加の精度で格納されることがあるが、float32(x)x の値を32ビット精度に丸めた結果を表す。同様に、x + 0.1 は32ビットを超える精度を使用することがあるが、float32(x + 0.1) はそうではない。

浮動小数点値または複素数値を含むすべての非定数変換において、結果の型が値を表現できない場合、変換は成功するが結果の値は実装依存である。


解説

ルール1:整数型間の変換

整数型間の変換は、ビット幅の異なる型の間で行われます。内部的には「拡張してから切り捨てる」という2段階の処理です。

小さい型 → 大きい型(拡張)

var x int8 = -1           // 0xFF(8ビット)
y := int32(x)              // 0xFFFFFFFF = -1(符号拡張)
fmt.Println(y)             // -1

var u uint8 = 255          // 0xFF(8ビット)
v := uint32(u)             // 0x000000FF = 255(ゼロ拡張)
fmt.Println(v)             // 255

符号付き整数は「符号拡張」(上位ビットを符号ビットで埋める)、符号なし整数は「ゼロ拡張」(上位ビットを0で埋める)されます。

大きい型 → 小さい型(切り捨て)

var x int32 = 256
y := int8(x)       // 下位8ビットだけ残る → 0
fmt.Println(y)      // 0

var z int32 = 257
w := int8(z)        // 下位8ビット → 1
fmt.Println(w)      // 1

値が収まりきらない場合、上位ビットが単純に切り捨てられます。エラーもパニックも起きないので注意が必要です。

原文の例を丁寧に追う

原文の uint32(int8(v)) == 0xFFFFFFF0 を段階的に見てみましょう。

v := uint16(0x10F0)     // v = 0x10F0 = 0001 0000 1111 0000(16ビット)

// ステップ1:uint16 → int8(切り捨て)
int8(v)                  // 下位8ビット 0xF0 = 1111 0000 → int8 では -16

// ステップ2:int8 → uint32(符号拡張)
uint32(int8(v))          // -16 を符号拡張 → 0xFFFFFFF0

0xF0int8 として解釈すると最上位ビットが1なので負の数(-16)です。これを uint32 に変換すると、符号拡張で上位ビットが全部1で埋められ、0xFFFFFFF0 になります。

ルール2:浮動小数点 → 整数

小数部分が切り捨てられます(ゼロに向かう切り捨て)。

int(3.7)    // 3(小数部分を切り捨て)
int(-3.7)   // -3(ゼロに向かって切り捨て、-4 ではない)
int(0.9)    // 0

四捨五入ではないので注意してください。四捨五入が必要な場合は math.Round を使います。

n := int(math.Round(3.7))  // 4

ルール3:浮動小数点型への変換と精度

浮動小数点型に変換するとき、変換先の精度に丸められます。

var x float64 = 1.0 / 3.0   // 0.3333333333333333(float64 精度)
y := float32(x)               // 0.33333334(float32 精度に丸められる)

ここで面白いのは、Go のコンパイラは内部的に宣言された型より高い精度で値を保持できるという点です。

var x float32 = 1.0 / 3.0   // 内部的には float32 以上の精度で保持されることがある
z := x + 0.1                 // この計算も高い精度で行われる可能性がある
w := float32(x + 0.1)        // float32() で明示的に 32ビット精度に丸める

前回の浮動小数点演算子の節で学んだFMAの話とつながりますね。float32() で明示的に変換することで、「ここで丸めてください」という意思表示になります。

表現できない値の変換

結果の型で表現できない値を変換すると、変換自体は成功しますが結果は実装依存です。

var f float64 = math.MaxFloat64
x := float32(f)  // float32 で表現できない → 結果は実装依存(多くの場合 +Inf)

「実装依存」とは、Go の仕様としては特定の値を保証しないが、クラッシュはしないという意味です。実際の Go コンパイラ(gc)では、多くの場合 +Inf-Inf になります。

変換時の情報損失に注意

型変換はデータの情報を失う可能性があります。Go はこれを黙って行うので、プログラマが注意する必要があります。

// 精度の損失
var big int64 = 1<<53 + 1     // 9007199254740993
f := float64(big)              // 9007199254740992(float64 では正確に表現できない)
fmt.Println(int64(f) == big)   // false!

// 範囲の損失
var x int32 = 100000
y := int16(x)
fmt.Println(y)  // -31072(切り捨てで全然違う値に!)

安全に変換したい場合は、事前に範囲チェックをしましょう。

func safeInt16(v int32) (int16, bool) {
    if v < math.MinInt16 || v > math.MaxInt16 {
        return 0, false  // 範囲外
    }
    return int16(v), true
}

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

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

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました