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

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

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

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

スポンサーリンク

背景

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

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

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

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

演算子(Operators)

演算子はオペランドを組み合わせて式を構成する。

Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr  = PrimaryExpr | unary_op UnaryExpr .

binary_op  = "||" | "&&" | rel_op | add_op | mul_op .
rel_op     = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op     = "+" | "-" | "|" | "^" .
mul_op     = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .

unary_op   = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .

比較については別の箇所で説明する。その他の二項演算子については、シフト操作または型なし定数が関与する場合を除き、オペランドの型は同一でなければならない。定数のみが関与する操作については、定数式の節を参照のこと。

シフト操作を除き、一方のオペランドが型なし定数で他方が型なし定数でない場合、定数は暗黙的に他方のオペランドの型に変換される。

シフト式の右オペランドは整数型を持つか [Go 1.13]、uint 型の値で表現可能な型なし定数でなければならない。非定数シフト式の左オペランドが型なし定数である場合、シフト式がその左オペランドだけに置き換えられたと仮定した場合にとるであろう型に、まず暗黙的に変換される。

var a [1024]byte
var s uint = 33

// 以下の例の結果は64ビット int の場合のものである。
var i = 1<<s                   // 1 は int 型を持つ
var j int32 = 1<<s             // 1 は int32 型を持つ; j == 0
var k = uint64(1<<s)           // 1 は uint64 型を持つ; k == 1<<33
var m int = 1.0<<s             // 1.0 は int 型を持つ; m == 1<<33
var n = 1.0<<s == j            // 1.0 は int32 型を持つ; n == true
var o = 1<<s == 2<<s           // 1 と 2 は int 型を持つ; o == false
var p = 1<<s == 1<<33          // 1 は int 型を持つ; p == true
var u = 1.0<<s                 // 不正: 1.0 は float64 型を持ち、シフトできない
var u1 = 1.0<<s != 0           // 不正: 1.0 は float64 型を持ち、シフトできない
var u2 = 1<<s != 1.0           // 不正: 1 は float64 型を持ち、シフトできない
var v1 float32 = 1<<s          // 不正: 1 は float32 型を持ち、シフトできない
var v2 = string(1<<s)          // 不正: 1 は string に変換され、シフトできない
var w int64 = 1.0<<33          // 1.0<<33 は定数シフト式である; w == 1<<33
var x = a[1.0<<s]              // パニック: 1.0 は int 型を持つが、1<<33 は配列の境界を超える
var b = make([]byte, 1.0<<s)   // 1.0 は int 型を持つ; len(b) == 1<<33

// 以下の例の結果は32ビット int の場合のものであり、
// シフトがオーバーフローすることを意味する。
var mm int = 1.0<<s            // 1.0 は int 型を持つ; mm == 0
var oo = 1<<s == 2<<s          // 1 と 2 は int 型を持つ; oo == true
var pp = 1<<s == 1<<33         // 不正: 1 は int 型を持つが、1<<33 は int をオーバーフローする
var xx = a[1.0<<s]             // 1.0 は int 型を持つ; xx == a[0]
var bb = make([]byte, 1.0<<s)  // 1.0 は int 型を持つ; len(bb) == 0

解説

演算子の全体像

演算子は大きく分けて二項演算子(2つの値を結ぶ)と単項演算子(1つの値に作用する)があります。

// 二項演算子
x + y       // 加算
a && b      // 論理AND
n << 3      // 左シフト

// 単項演算子
-x          // 符号反転
!done       // 論理NOT
&obj        // アドレス取得
*ptr        // ポインタのデリファレンス
<-ch        // チャネルからの受信

演算子の優先順位

構文定義の中で mul_opadd_oprel_op と分けられているのは、優先順位を表しています。上のほうが優先度が高いです。

優先度(高→低)演算子
乗算系(mul_op* / % << >> & &^
加算系(add_op+ - | ^
比較系(rel_op== != < <= > >=
論理AND&&
論理OR||

算数の「掛け算は足し算より先」と同じルールが、ビット演算やシフトにも拡張されているイメージですね。

a + b * c     // b*c が先に計算される
a & b << c    // b<<c が先(シフトは乗算系の優先度)

オペランドの型は同一でなければならない

Go では、異なる型同士の演算は基本的にできません。

var a int = 10
var b float64 = 3.14

c := a + b  // コンパイルエラー! int と float64 は型が違う

他の言語では暗黙的に型変換してくれることがありますが、Go では明示的に変換する必要があります。

c := float64(a) + b  // OK! 明示的に変換

型なし定数の暗黙変換

ただし、型なし定数が絡む場合は例外です。型なし定数は「まだ型が決まっていないリテラル」なので、相手の型に自動的に合わせてくれます。

var x float64 = 3.14
y := x + 1  // 1 は型なし定数 → float64 に暗黙変換される → OK!

1 はリテラルなので型なし定数です。xfloat64 なので、1float64 として扱われます。

シフト演算子のルール

シフト演算子(<<>>)は特殊なルールを持っています。ここが Go の演算子で最もややこしい部分です。

右オペランド:シフト量

シフト量(何ビットずらすか)は整数型か、uint で表現可能な型なし定数でなければなりません。

var s uint = 3
x := 1 << s     // OK! s は uint
x := 1 << 3     // OK! 3 は uint で表現可能な型なし定数
x := 1 << -1    // コンパイルエラー! 負の数はダメ

左オペランド:型なし定数のとき

ここが一番ややこしいポイントです。左オペランドが型なし定数で、シフト量が変数(非定数)の場合、「シフト式がなかったとしたら、この定数はどの型になるか」を先に考えます。

var s uint = 33

var i = 1<<s           // 1 は何型? → 「var i = 1」なら int → 1 は int 型
var j int32 = 1<<s     // 1 は何型? → 「var j int32 = 1」なら int32 → 1 は int32 型

原文の例を分解して見てみましょう。

var m int = 1.0<<s     // 1.0 は何型? → 「var m int = 1.0」なら int → OK(1.0 は int として表現可能)
var u = 1.0<<s         // 1.0 は何型? → 「var u = 1.0」なら float64 → シフトできない! ❌

1.0 は一見整数に見えますが、文脈によって型が変わります。var m int = 1.0 なら int として扱えますが、var u = 1.0 だとデフォルト型の float64 になり、浮動小数点数はシフトできないのでエラーになります。

もう少しわかりやすい例で

シフトの型ルールを簡単な例でまとめてみます。

var s uint = 33

// ✅ 成功するケース
1 << s              // 1 → int(デフォルト型)→ シフトOK
int32(1) << s       // 1 → int32(明示的に指定)→ シフトOK
1.0 << 33           // 定数式 → コンパイル時に計算されるのでOK

// ❌ 失敗するケース
1.0 << s            // 1.0 → float64(デフォルト型)→ シフトできない
float32(1) << s     // 1 → float32(明示的に指定)→ シフトできない

ポイントは「シフトは整数型にしかできない」というルールです。型なし定数のデフォルト型が float64float32 になってしまうとシフトできません。

定数シフト式 vs 非定数シフト式

シフト量が定数かどうかで挙動が変わります。

// 定数シフト式:コンパイル時に完全に計算される
var w int64 = 1.0<<33  // OK! 1.0<<33 はコンパイル時に 8589934592 と計算される

// 非定数シフト式:実行時に計算される → 型の制約が厳しい
var s uint = 33
var u = 1.0<<s         // エラー! 実行時のシフトでは float64 は不可

定数シフト式はコンパイル時に結果が確定するので、コンパイラが直接計算して型をチェックできます。非定数シフト式は実行時にしか結果がわからないので、より厳しいルールが適用されます。

32ビット vs 64ビットでの違い

原文の後半に32ビット int の場合の例がありますね。Go の int のサイズはプラットフォーム依存(32ビットまたは64ビット)なので、シフト結果がオーバーフローするかどうかが変わります。

var s uint = 33

// 64ビット int の場合
var m int = 1.0<<s    // 1<<33 = 8589934592 → int に収まる → m == 8589934592

// 32ビット int の場合
var mm int = 1.0<<s   // 1<<33 は 32ビット int をオーバーフロー → mm == 0

現代のシステムではほぼ64ビットですが、組み込みシステムなど32ビット環境もまだ存在します。ポータブルなコードを書くなら、int のサイズに依存しないよう int64uint64 を明示的に使うことを検討しましょう。

おわりに 

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

よっしー
よっしー

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

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

コメント

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