
こんにちは。よっしーです(^^)
本日は、Go言語の言語仕様について解説しています。
背景
Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。
言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。
そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。
言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!
システムに関する考慮事項
unsafe パッケージ
組み込みパッケージ unsafe は、コンパイラに認識されており、インポートパス "unsafe" を通じてアクセスできます。これは、型システムを破る操作を含む、低レベルプログラミングのための機能を提供します。unsafe を使用するパッケージは、型安全性について手作業で検証されなければならず、移植可能ではない場合があります。このパッケージは次のインターフェースを提供します。
package unsafe
type ArbitraryType int // 任意の Go の型の略記。実在する型ではない。
type Pointer *ArbitraryType
func Alignof(variable ArbitraryType) uintptr
func Offsetof(selector ArbitraryType) uintptr
func Sizeof(variable ArbitraryType) uintptr
type IntegerType int // 整数型の略記。実在する型ではない。
func Add(ptr Pointer, len IntegerType) Pointer
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte
Pointer はポインタ型ですが、Pointer の値は参照外し(デリファレンス)できません。基底型が uintptr である任意のポインタまたは値は、基底型が Pointer である型へ変換でき、その逆も可能です。それぞれの型が型パラメータである場合、それぞれの型集合に含まれるすべての型が同じ基底型を持たなければならず、それは前者では uintptr、後者では Pointer でなければなりません。Pointer と uintptr の間の変換の効果は、実装定義です。
var f float64
bits = *(*uint64)(unsafe.Pointer(&f))
type ptr unsafe.Pointer
bits = *(*uint64)(ptr(&f))
func f[P ~*B, B any](p P) uintptr {
return uintptr(unsafe.Pointer(p))
}
var p ptr = nil
関数 Alignof と Sizeof は、任意の型の式 x を取り、var v = x のように仮想的な変数 v が宣言されたと仮定して、その v のアラインメントまたはサイズをそれぞれ返します。
関数 Offsetof は、(場合によっては括弧でくくられた)セレクタ s.f を取ります。これは s または *s が表す構造体のフィールド f を表し、その構造体のアドレスを基準としたフィールドのオフセットをバイト単位で返します。f が埋め込みフィールドである場合、それは構造体のフィールドを通じてポインタの間接参照なしに到達可能でなければなりません。フィールド f を持つ構造体 s について、次が成り立ちます。
uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))
コンピュータのアーキテクチャは、メモリアドレスがアラインされていること、すなわち変数のアドレスがある係数(その変数の型のアラインメント)の倍数であることを要求する場合があります。関数 Alignof は、任意の型の変数を表す式を取り、その変数(の型)のアラインメントをバイト単位で返します。変数 x について、次が成り立ちます。
uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0
型 T(の変数)は、T が型パラメータである場合、または可変サイズの要素やフィールドを含む配列型や構造体型である場合に、可変サイズを持ちます。それ以外の場合、サイズは定数です。Alignof・Offsetof・Sizeof の呼び出しは、それらの引数(Offsetof の場合はセレクタ式 s.f の構造体 s)が定数サイズの型であれば、uintptr 型のコンパイル時定数式となります。
関数 Add は len を ptr に加算し、更新されたポインタ unsafe.Pointer(uintptr(ptr) + uintptr(len)) を返します [Go 1.17]。len 引数は整数型または型なし定数でなければなりません。定数の len 引数は int 型の値で表現可能でなければならず、型なし定数の場合は int 型が与えられます。Pointer の有効な使い方に関するルールは引き続き適用されます。
関数 Slice は、基底配列が ptr から始まり、長さと容量が len であるスライスを返します。Slice(ptr, len) は次と等価です。
(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
ただし特別なケースとして、ptr が nil かつ len が 0 の場合、Slice は nil を返します [Go 1.17]。
len 引数は整数型または型なし定数でなければなりません。定数の len 引数は非負であり、int 型の値で表現可能でなければなりません。型なし定数の場合は int 型が与えられます。実行時に、len が負である場合、または ptr が nil かつ len が 0 でない場合、実行時パニックが発生します [Go 1.17]。
関数 SliceData は、スライス引数の基底配列へのポインタを返します。スライスの容量 cap(slice) が 0 でない場合、そのポインタは &slice[:1][0] です。スライスが nil の場合、結果は nil です。それ以外の場合は、規定されていないメモリアドレスを指す非 nil のポインタです [Go 1.20]。
関数 String は、基底バイト列が ptr から始まり、長さが len である文字列値を返します。ptr と len の引数には、関数 Slice と同じ要件が適用されます。len が 0 の場合、結果は空文字列 "" です。Go の文字列は不変(immutable)であるため、String に渡されたバイト列は、その後変更してはなりません。[Go 1.20]
関数 StringData は、str 引数の基底バイト列へのポインタを返します。空文字列の場合、戻り値は規定されておらず、nil である場合があります。Go の文字列は不変であるため、StringData が返すバイト列は変更してはなりません [Go 1.20]。
解説
結論:unsafe パッケージは、Goの「型安全」という保護を意図的に外して、メモリを直接いじる低レベル操作を可能にする特殊な道具箱。強力だが危険で、移植性も型安全性も自分で保証する必要がある。初心者は基本的に使わなくてよい。
これはGo言語仕様の最後の節で、内容は一気に上級者向けになります。初心者が実務で使うことはまずないので、「こういう抜け道が存在する」という存在を知っておけば十分です。細部を理解する必要はありません。
unsafe とは何か、なぜ「unsafe(安全でない)」なのか
Goは普段、「型安全」という強い保護を提供します。たとえば「int を勝手に別の型として読み替える」「メモリ上の好きな場所を直接読み書きする」といった危険な操作は、通常はコンパイラが禁止します。これがGoの安全性の根幹です。
unsafe パッケージは、その保護を意図的に外すための道具箱です。名前が unsafe(安全でない)とはっきり付いているのは、「これを使うと自分で安全性に責任を持つことになりますよ」という警告です。
車の運転に例えると、Goの通常のコードは「自動ブレーキやエアバッグが効いた状態での運転」です。unsafe はそれらの安全装置をオフにして、自分の判断だけで運転する状態に相当します。速く走れる場面もありますが、事故れば全部自己責任です。
原文も明確に2つの代償を挙げています。第一に、型安全性を手作業で検証しなければならない(コンパイラはもう守ってくれない)。第二に、移植可能でない場合がある(あるCPUでは動いても別のCPUでは壊れるかもしれない)。
中心となる unsafe.Pointer
このパッケージの核は unsafe.Pointer という特殊なポインタ型です。
通常のGoのポインタは「型」に縛られます。*int(int へのポインタ)は int しか指せず、*float64 とは互換性がありません。これが型安全の一部です。
unsafe.Pointer は、この縛りを外した**「型を問わない万能ポインタ」**です。これを経由すると、本来は変換できない型同士を無理やり変換できます。
原文の最初のコード例がまさにその典型です。
var f float64
bits = *(*uint64)(unsafe.Pointer(&f))
これは「float64 の変数 f が、メモリ上でどんなビット列になっているか」を、uint64(符号なし64ビット整数)として読み出しています。float64 と uint64 はまったく別の型で、通常は相互変換できません。しかし両者ともメモリ上では64ビットを占めるので、unsafe.Pointer を経由して「同じ64ビットを別の型の目で見る」ことを可能にしています。
レコード盤に例えると、同じ溝(メモリ上のビット列)を、ある針(float64)で読むと音楽に、別の針(uint64)で読むと数値に聞こえる、というような「同じ実体を違う型として再解釈する」操作です。
重要な制約として、原文は Pointer の値は参照外し(デリファレンス)できないと述べています。unsafe.Pointer そのものから直接中身を取り出すことはできず、いったん通常の型付きポインタ(上の例の *uint64)に変換し直してから中身を読む、という手順を踏みます。
サイズと配置を調べる3つの関数
Sizeof・Alignof・Offsetof は、メモリ上のレイアウトを調べる関数です。これらは比較的おとなしく、メモリを壊すわけではありません。
| 関数 | 役割 |
|---|---|
Sizeof(x) | x がメモリ上で占めるバイト数 |
Alignof(x) | x の型に要求される「アラインメント(境界)」のバイト数 |
Offsetof(s.f) | 構造体 s の先頭から、フィールド f までの距離(バイト) |
アラインメントという概念が初心者には馴染みがないので補足します。多くのCPUは、データを「特定の倍数のアドレス」に配置することを要求または好みます。たとえば8バイトのデータは8の倍数のアドレスに置く、といった具合です。原文の式 uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0 は、「変数 x のアドレスはアラインメント値で割り切れる(=倍数になっている)」ことを表しています。
駐車場に例えると、大型車は決められた区画の頭にぴったり停めなければならない、というルールのようなものです。CPUがデータを効率よく読むための、ハードウェア由来の制約です。
オフセットは、構造体の中でのフィールドの位置です。原文の式は、構造体の先頭アドレスにフィールドのオフセットを足すと、そのフィールド自身のアドレスになる、という当たり前の関係を保証しています。
構造体 s のアドレス + s.f のオフセット == s.f のアドレス
ポインタ演算とメモリの再解釈(Add / Slice / String など)
残りの関数は、より踏み込んだ低レベル操作です。要点だけ押さえます。
Add はポインタに数値を足してアドレスをずらす関数です。これは「ポインタ演算」と呼ばれ、C言語などにはありますが、Goが通常は禁止している危険な操作です。一歩間違えると、確保していないメモリ領域を指してしまいます。
Slice / String は、生のメモリ領域(ポインタ+長さ)を、Goのスライスや文字列として再解釈する関数です。逆に SliceData / StringData は、スライスや文字列から内部の生ポインタを取り出す関数です。
// 例:ptr から始まる len 個分のメモリを []int として扱う
s := unsafe.Slice(ptr, len)
原文が随所で警告している重要な点が2つあります。
第一に、実行時パニックの危険です。たとえば Slice では、len が負だったり、ptr が nil なのに len が 0 でなかったりすると、実行時パニックが起きます。前の節で学んだパニックがここでも登場します。
第二に、**Goの文字列は不変(immutable)**だという原則です。String に渡したバイト列や、StringData で取り出したバイト列を後から書き換えてはいけません。文字列は変更されない前提でGoの内部が最適化されているため、この約束を破るとプログラムが予測不能な壊れ方をします。
[Go 1.17] などの表記について
原文に [Go 1.17] [Go 1.20] という注記が付いています。これはその機能がGoのどのバージョンで追加されたかを示すものです。たとえば Add や Slice はGo 1.17から、String や StringData はGo 1.20から使えるようになりました。古いバージョンのGoでは使えない、という意味です。
初心者が取るべきスタンス
最後に、この節全体に対する実用的な姿勢をまとめます。
結論として、初心者(そして大半の実務エンジニア)は unsafe を使う必要がありません。 Goの通常の機能だけで、ほとんどすべてのプログラムは型安全に書けます。
unsafe が実際に使われるのは、極めて限られた場面です。具体的には、C言語との連携(cgo)、メモリコピーを避けてパフォーマンスを極限まで高めたいライブラリの内部、低レベルなシステムプログラミングなどです。これらは標準ライブラリや一部の高性能ライブラリの「奥の方」で慎重に使われており、アプリケーションを書く側が直接触ることはまれです。
道具箱に例えるなら、unsafe は「専門の職人が特殊な状況でのみ使う、扱いを誤れば怪我をする工具」です。存在と役割を知っておくのは有益ですが、必要に迫られるまで手に取らないのが賢明です。
補足:これでGo言語仕様(The Go Programming Language Specification)の主要な節を一通り読み終えたことになります。unsafe は仕様の最後を飾る、いわば「安全な言語であるGoが用意した、あえての非常口」です。Goが普段どれだけ多くの危険な操作を裏で禁止して私たちを守ってくれているか、その保護のありがたみを逆説的に教えてくれる節でもあります。お疲れさまでした。次に学ぶとしたら、仕様書を離れて、標準ライブラリ(fmt、net/http、encoding/json など)の使い方や、実際に小さなプログラムを書いてみることをおすすめします。
おわりに
本日は、Go言語の言語仕様について解説しました。

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

コメント