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

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

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

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

スポンサーリンク

背景

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

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

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

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

変換(Conversions)

変換は、式の型を変換で指定された型に変更する。変換はソース内にリテラルとして現れることも、式が現れる文脈によって暗黙的に行われることもある。

明示的な変換は T(x) の形式の式であり、T は型、x は型 T に変換可能な式である。

Conversion = Type "(" Expression [ "," ] ")" .

型が演算子 * または <- で始まる場合、または型がキーワード func で始まり結果リストを持たない場合、曖昧さを避けるために必要に応じて括弧で囲まなければならない:

*Point(p)        // *(Point(p)) と同じ
(*Point)(p)      // p は *Point に変換される
<-chan int(c)    // <-(chan int(c)) と同じ
(<-chan int)(c)  // c は <-chan int に変換される
func()(x)        // 関数シグネチャ func() x
(func())(x)      // x は func() に変換される
(func() int)(x)  // x は func() int に変換される
func() int(x)    // x は func() int に変換される(曖昧さなし)

定数値 x は、xT の値で表現可能である場合に型 T に変換できる。特殊なケースとして、整数定数 x は非定数の x と同じ規則を使用して文字列型に明示的に変換できる。

定数を型パラメータでない型に変換すると、型付き定数が生成される。

uint(iota)               // uint 型の iota 値
float32(2.718281828)     // float32 型の 2.718281828
complex128(1)            // complex128 型の 1.0 + 0.0i
float32(0.49999999)      // float32 型の 0.5
float64(-1e-1000)        // float64 型の 0.0
string('x')              // string 型の "x"
string(0x266c)           // string 型の "♬"
myString("foo" + "bar")  // myString 型の "foobar"
string([]byte{'a'})      // 定数ではない: []byte{'a'} は定数ではない
(*int)(nil)              // 定数ではない: nil は定数ではなく、*int は真偽値型、数値型、文字列型ではない
int(1.2)                 // 不正: 1.2 は int として表現できない
string(65.0)             // 不正: 65.0 は整数定数ではない

定数を型パラメータに変換すると、その型の非定数値が生成され、値は型パラメータがインスタンス化される型引数の値として表現される。たとえば、以下の関数が与えられたとき:

func f[P ~float32|~float64]() {
	… P(1.1) …
}

変換 P(1.1) は型 P の非定数値を生成し、値 1.1f の型引数に応じて float32 または float64 として表現される。したがって、ffloat32 型でインスタンス化された場合、式 P(1.1) + 1.2 の数値は対応する非定数の float32 加算と同じ精度で計算される。

非定数値 x は、以下のいずれかの場合に型 T に変換できる:

  • xT に代入可能である。
  • 構造体タグを無視して(以下を参照)、x の型と T が型パラメータでなく、同一の基底型を持つ。
  • 構造体タグを無視して、x の型と T が名前付き型でないポインタ型であり、そのポインタ基底型が型パラメータでなく、同一の基底型を持つ。
  • x の型と T がともに整数型または浮動小数点型である。
  • x の型と T がともに複素数型である。
  • x が整数、またはバイトもしくはルーンのスライスであり、T が文字列型である。
  • x が文字列であり、T がバイトまたはルーンのスライスである。
  • x がスライスであり、T が配列 [Go 1.20] または配列へのポインタ [Go 1.17] であり、スライスと配列の型が同一の要素型を持つ。

さらに、T または x の型 V が型パラメータである場合、以下の条件のいずれかが当てはまれば、x は型 T にも変換できる:

  • VT がともに型パラメータであり、V の型集合の各型の値が T の型集合の各型に変換できる。
  • V のみが型パラメータであり、V の型集合の各型の値が T に変換できる。
  • T のみが型パラメータであり、xT の型集合の各型に変換できる。

変換のための構造体型の同一性比較において、構造体タグは無視される:

type Person struct {
	Name    string
	Address *struct {
		Street string
		City   string
	}
}

var data *struct {
	Name    string `json:"name"`
	Address *struct {
		Street string `json:"street"`
		City   string `json:"city"`
	} `json:"address"`
}

var person = (*Person)(data)  // タグを無視すれば、基底型は同一

数値型間の(非定数の)変換、または文字列型との間の変換には特定のルールが適用される。これらの変換は x の表現を変更する可能性があり、実行時コストが発生する。その他のすべての変換は型のみを変更し、x の表現は変更しない。

ポインタと整数の間の変換のための言語機構は存在しない。unsafe パッケージが制限された状況下でこの機能を実装する。


解説

型変換の基本

Go は暗黙的な型変換を行わない言語です。異なる型の間で値を使いたいときは、明示的に変換する必要があります。

var i int = 42
var f float64 = float64(i)  // int → float64 に明示的に変換
var u uint = uint(f)         // float64 → uint に明示的に変換

書き方は 型名(値) です。関数呼び出しのように見えますが、型変換です。

括弧が必要になるケース

型名が *<- で始まったり、func で始まるときは、構文が曖昧になる場合があります。

// これはどう解釈される?
*Point(p)

これは *(Point(p))pPoint に変換してからデリファレンス)と解釈されます。p*Point に変換したい場合は、型全体を括弧で囲みます。

(*Point)(p)   // p を *Point 型に変換

実用上よく出会うのはチャネル型の変換です。

(<-chan int)(c)  // c を受信専用チャネルに変換

定数の変換

定数を変換するとき、変換先の型で値が表現できるかどうかがチェックされます。

// OK:表現可能
float32(2.718281828)  // 精度は落ちるが float32 で表現できる
complex128(1)          // 1.0 + 0.0i になる
string('x')            // "x" になる
string(0x266c)         // "♬" になる(Unicode コードポイント → 文字)

// エラー:表現不可能
int(1.2)     // 1.2 は整数として表現できない
string(65.0) // 65.0 は整数定数ではないので文字列に変換できない

float32(0.49999999)0.5 になるのは、float32 の精度では 0.49999999 を正確に表現できず、最も近い値 0.5 に丸められるからです。

非定数の変換ルール

非定数の値を変換できるケースを、よく使うものから見ていきましょう。

1. 数値型間の変換

var i int = 42
var f float64 = float64(i)  // int → float64
var u uint8 = uint8(i)      // int → uint8(値が範囲外なら切り捨て)

2. 文字列とバイト/ルーンスライスの相互変換

s := "Hello, 世界"
b := []byte(s)    // 文字列 → バイトスライス
r := []rune(s)    // 文字列 → ルーンスライス

s2 := string(b)   // バイトスライス → 文字列
s3 := string(r)   // ルーンスライス → 文字列

日本語を扱うときは []rune への変換が重要です。

s := "こんにちは"
fmt.Println(len(s))          // 15(バイト数)
fmt.Println(len([]rune(s)))  // 5(文字数)

3. 同じ基底型を持つ型間の変換

type Celsius float64
type Fahrenheit float64

var c Celsius = 100
var f Fahrenheit = Fahrenheit(c)  // 基底型が同じ float64 なので変換可能

4. スライスから配列への変換(Go 1.17/1.20)

s := []int{1, 2, 3, 4, 5}

// スライス → 配列へのポインタ(Go 1.17)
ap := (*[3]int)(s)  // 先頭3要素を指す配列ポインタ

// スライス → 配列(Go 1.20)
a := [3]int(s)      // 先頭3要素をコピーした配列

スライスの長さが配列の長さより短い場合はパニックします。

構造体タグは無視される

JSON タグなどの構造体タグは、変換時の型の一致判定では無視されます。

type Person struct {
    Name string
}

var data struct {
    Name string `json:"name"`
}

p := Person(data)  // タグが違うだけなので変換OK

これは、外部APIからのデータ(タグつき構造体)を内部の型(タグなし構造体)に変換する際に便利です。

変換のコスト

すべての型変換が同じコストではありません。

表現が変わる変換(コストあり):

float64(42)       // int → float64:ビット表現が完全に変わる
[]byte("hello")   // 文字列 → バイトスライス:データのコピーが発生
string([]byte{})  // バイトスライス → 文字列:データのコピーが発生

型だけ変わる変換(コストなし):

type MyInt int
MyInt(42)          // 同じ基底型なので、メモリ上の表現は変わらない

後者は、コンパイラが「これは別の型として扱ってね」と認識するだけで、実行時には何も起きません。

ポインタと整数の変換はできない

Go では安全性のため、ポインタを整数に変換したり、整数をポインタに変換したりすることはできません。

var p *int = &x
n := int(p)    // コンパイルエラー!

どうしても必要な場合(OSとのやりとりなど)は unsafe.Pointer を使いますが、名前のとおり「安全ではない」ので、通常のアプリケーション開発では避けるべきです。

おわりに 

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

よっしー
よっしー

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

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

コメント

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