Goでつまづいたところを仕様を見ながら理解してみる
Go言語デビューしました!🎉 シンプルなのもあってすぐに馴染むことができています。Goよいですね!
さてさて今回はGoを書いていて「これできるんだ」「これはコンパイルエラーなんだ」となったところをGoの仕様などを見ながら(できればそのwhyまで)理解したいと思います。 Go 1.18時点での記事です。
つまづき1:constをポインタとして扱えない
例えばこんなのがあったとして
const Ten = 10 type User struct { age *int }
以下のコンパイルが通りません。
func main() { user := User{ &Ten, // コンパイル通らない🤔 } fmt.Println(user) }
エラーメッセージを見てみると
invalid operation: cannot take address of Ten (constant 10 of type Age)
とのこと。constのアドレスを取ることはできないらしい。
Goの仕様を見てみる
&
オペレーターに関する仕様を見てみます。どうやらここが該当していそうです。
The Go Programming Language Specification - The Go Programming Language
The operand must be addressable , that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array.
たしかに&
オペレーターを取ることのできるオペランドにconstantsはいないですね。constantは変数(variable)ではないことに注意ですね。
なんでconstのポインタとして扱えないの?
ここに答えがありそうです。
Taking address of string literal
リンク先はstring
の例ですがint
もリテラルという意味では同じ、かつconstはリテラルで表現されるものなので同様のことが言えそうです。
以下のような関数があるとして
func changeIntLiteral(s *int) { *s = 1 }
// これはできない changeIntLiteral(&10) // もちろんこれもできない const Ten = 10 changeIntLiteral(&Ten)
はできないということですね。
これを許してしまうとリテラル(変数の値ではなく)が直接書き換えられてしまうというよくわからないことが起きてしまいます。また、定数であるはずのconstの値を書き換えることが可能になってしまいます。なのでconstのアドレスを取ることはできないし、ポインタとして扱うこともできない、ということみたいです。
じゃあポインタとして扱いたい場合はどうするの?
リテラルを直接書き換えるようなことをしなければいいので以下の方法で解決できそうです。
- 一度変数に入れてそのポインタを渡す
- 関数を経由する
先程の例なら
const Ten = 10 type User struct { age *int }
一度変数に取る場合はこんな感じ。
ten := Ten
user := User{
&ten, // コンパイル通る🤗
}
const Ten
と同じ値の変数を作りそのポインタを渡しています。
関数を経由する場合はこんな感じ。
func ToPtrInt(x int) *int { return &x } user := User{ ToPtrInt(Ten) }
これも変数のときと原理は同じで、x
という変数がconst Ten
と同じ値を持ちそのポインタを返しています。
ちなみにGo 1.18以降ならジェネリクスを使ってより汎用的に書けます。
func ToPtr[T any](x T) *T { return &x }
参考資料
- constで定義した値(string)をポインタとして扱おうと思ったら怒られた - 寝ても覚めてもこんぴうた
- pointers - Find address of constant in go - Stack Overflow
- Taking address of string literal
つまづき2:[]int
を type X []int
に代入できる
以下はコンパイルエラーになります。
type MyInt int func returnMyInt(myInt MyInt) MyInt { return myInt } func main() { three := 3 v := returnMyInt(three) // ここでコンパイルエラー fmt.Println(v) }
エラーメッセージは
cannot use three (variable of type int) as type MyInt in argument to returnMyInt
で、「int
型の変数をMyInt
型として使うことはできないよ」と言っています。これは納得です。
では次の例です。
type MyIntSlice []int func first(myIntSlice MyIntSlice) int { return myIntSlice[0] } func main() { arr := []int{1, 2, 3} v := first(arr) // コンパイルエラーにならない!? fmt.Println(v) // 1 }
先ほどの例と異なるのはint
型ではなく[]int
型を扱っているところです。先ほどの例と同様にコンパイルエラーになるのかなと思いきや、こちらは通ります。
この謎を追ってみたいと思います。
代入可能性
int
の例ではMyInt
型の変数にint
型の変数を代入しようとしてコンパイルエラーになっています。一方、[]int
型の例ではMyIntSlice
型の変数に[]int
型の変数を代入しようとしてコンパイルエラーにはならずに通りました。
つまり「代入」について知る必要がありそうです。Goの仕様書に「Assignability(代入可能性)」というまさにドンピシャの記述があります。
The Go Programming Language Specification - The Go Programming Language
引用すると(番号は勝手につけました)
A value
x
of typeV
is assignable to a variable of typeT
("x
is assignable toT
") if one of the following conditions applies:
V
andT
are identical.V
andT
have identical underlying types but are not type parameters and at least one ofV
orT
is not a named type.V
andT
are channel types with identical element types,V
is a bidirectional channel, and at least one ofV
orT
is not a named type.T
is an interface type, but not a type parameter, andx
implementsT
.x
is the predeclared identifiernil
andT
is a pointer, function, slice, map, channel, or interface type, but not a type parameter.x
is an untyped constant representable by a value of typeT
.
6つある条件のうち1つを満たせば代入可能ということみたいですね。つまり[]int
, MyIntSlice
の例はこれらのどれか1つを満たすはずです。
1つずつ見ていきたいところですが、3, 4, 5については今回の例とは明らかに関係なさそうなので割愛します。
1. V
and T
are identical.
では代入可能性の1つめの条件について考えてみます。
identicalってなに?
条件の中にidenticalというワードが出てきました。日本語だと「同一の」という形容詞です。Go言語における「同一」とはなんでしょうか?
こちらにその記述があります。
The Go Programming Language Specification - The Go Programming Language
A named type is always different from any other type.
named typeというものは常に他の型とは異なる(つまりidenticalじゃない)そうです。2つの型がnamed typeでない場合の条件もありますが今回は問題とは関係ないので割愛します。詳しくはぜひ仕様書をご覧ください!
named typeってなに?
named typeがなにかというとこちらに記述があります。
The Go Programming Language Specification - The Go Programming Language
Predeclared types, defined types, and type parameters are called named types . An alias denotes a named type if the type given in the alias declaration is a named type.
defined typeはnamed typeの1つみたいです。defined typeというのは雑に言うと
type 名前 型
みたいに定義された型です(より正確な定義はこちら https://go.dev/ref/spec#Type_declarations)。ということはMyIntSlice
型はdefined typeですね!
では[]int型
はnamed typeなのでしょうか?defined typeでも型パラメータでもないのでPredeclared typeかどうかが判断できればよさそうです。
Predeclared typeの一覧はこちらです。
The Go Programming Language Specification - The Go Programming Language
[]int
型はPredeclared typeではないようですね。なのでnamed typeではありません。
ちなみに[]int
型はtype literal(型リテラル)と呼ぶそうです(https://go.dev/ref/spec#Type)。
MyIntSlice
型と[]int
型は条件1を満たすか?
MyIntSlice
型はdefined type、つまりnamed typeのひとつであり、常に他の型とは異なるので[]int
とはidenticalではないですね。
よってこの2つの型は条件を満たしません。
2. V
and T
have identical underlying types but are not type parameters and at least one of V
or T
is not a named type.
続いて代入可能性の2つめの条件について見てみます。
underlying typesという見慣れないワードが出てきました。これについては後から見るとして後半部分を見てみます。
but are not type parameters
まず今回の例はどちらも型パラメータがからんでくる話ではないのでこれは満たしています。
では次の
at least one of
V
orT
is not a named type.
を見てみます。MyIntSlice
型はdefined typeなのでnamed typeのひとつです。一方[]int
型は型リテラルでnamed typeではありません。
よって
but are not type parameters and at least one of
V
orT
is not a named type
の部分はMyIntSlice
型と[]int
型については満たしていることがわかります。
では条件の前半部分に戻って
V
andT
have identical underlying types
について考えてみます。
MyIntSlice
型と[]int
型のunderlying type?あとそれがidentical?
underlying typeについて述べている部分はこちらです。
The Go Programming Language Specification - The Go Programming Language
Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its declaration.
T
型というのはunderlying typeを持っているようです。型リテラルである[]int
型はそれ自身がunderlying typeなので[]int
型です。
一方、MyIntSlice
型の定義は
type MyIntSlice []int
で、underlying typeは[]int
型です。
ということでMyIntSlice
型と[]int
型のunderlying typeはどちらも[]int
型です。そしてこれは明らかにidenticalです。
MyIntSlice
型と[]int
型は条件2を満たすか?
ここまでみてきた通り条件2の
V
andT
have identical underlying types but are not type parameters and at least one ofV
orT
is not a named type.
をMyIntSlice
型と[]int
型は満たします。
よって代入可能性の条件を満たすので[]int
型の変数をMyIntSlice
型の変数に代入することができます!
6. x
is an untyped constant representable by a value of type T
.
代入可能性を満たすことはわかりましたが、せっかくなのでこの条件についても触れておこうと思います。
xはuntyped constantであると。ただのuntyped constantではなくてrepresentable by a value of type T なuntyped constantであると。
MyIntSlice
型と[]int
型の例ではconstを扱っていないのでこの条件は満たさないですがせっかくなので見てみます。
untyped constantってなに?
仕様書のconstantに記述があります。
The Go Programming Language Specification - The Go Programming Language
定義を正確に書くとちょっと大変そうですが、仕様書にある例を見るとどんなものがtypedもしくはuntypedなのかわかりそうです。 一部を抜粋してみます。
const a = 2 + 3.0 // a == 5.0 (untyped floating-point constant) const b = 15 / 4 // b == 3 (untyped integer constant) const c = 15 / 4.0 // c == 3.75 (untyped floating-point constant) const Θ float64 = 3/2 // Θ == 1.0 (type float64, 3/2 is integer division) const Π float64 = 3/2. // Π == 1.5 (type float64, 3/2. is float division) const k = 'w' + 1 // k == 'x' (untyped rune constant) const m = string(k) // m == "x" (type string)
型を明記するかstring関数のようなものを使わなければuntypedになりそうというざっくりイメージです。
representableってなに?
こちらにその仕様があります。
The Go Programming Language Specification - The Go Programming Language
日本語だと「表現可能な」みたいな感じでしょうか。
A constant
x
is representable by a value of typeT
, whereT
is not a type parameter, if one of the following conditions applies:
x
is in the set of values determined byT
.T
is a floating-point type andx
can be rounded toT
's precision without overflow. Rounding uses IEEE 754 round-to-even rules but with an IEEE negative zero further simplified to an unsigned zero. Note that constant values never result in an IEEE negative zero, NaN, or infinity.T
is a complex type, andx
's componentsreal(x)
andimag(x)
are representable by values ofT
's component type (float32
orfloat64
).
日本語に雑に訳すと以下のようなかんじでしょうか。
定数xが型Tの値によってrepresentableとは、Tが型パラメータでなく以下の条件のうち1つを満たすもの
- Tによって決定される値の集合にxが含まれる
- Tがfloating-point型で、xをオーバーフローせずにTの精度で丸めることができる
- Tがcomplex型で、xの要素がTの要素の型の値で表現できる
おまけ感ありますが条件6についてもざっくり眺めてみました。
なんでこういう仕様なの?
なぜMyIntSlice
型に[]int
型を代入できるようになっているのかについてのソースを見つけられなかったのでこれで終わってもいいのですが、せっかくなので考察してみます。
MyInt
型にint
型を代入できないように、MyIntSlice
型に[]int
型を代入できない方がよくない?と感覚的には思ったりします。
ですがGo言語でそのようにするのは無理なのでは?という考えを書いてみます。以下は正しいかどうかの自信はなく誤っている可能性も大いにあることにご留意ください!間違い訂正大歓迎です!
MyInt
型とint
型の代入可能性とMyIntSlice
型と[]int
型の代入可能性、差分はどこか
MyIntSlice
型と[]int
型では代入可能性の以下の条件を満たしたため代入可能でした。
V
andT
have identical underlying types but are not type parameters and at least one ofV
orT
is not a named type.
この条件をMyInt
型とint
型は満たさないわけですが、差分はどこかというと
at least one of
V
orT
is not a named type.
の部分です。MyInt
型とint
型はともにnamed typedです。named typeの条件は
Predeclared types, defined types, and type parameters are called named types . An alias denotes a named type if the type given in the alias declaration is a named type.
で、int
型はpredeclared typeのためです。一方[]int
型は型リテラルなのでnamed typeではありません。
型リテラルをdefined typeにできればMyIntSlice
型と[]int
型も代入不可能にできそう → 無理では?
型リテラルとはmapやスライスのように他の型によって定義される型です。なのでdefined typeにするのに他の型が必要です。型は無限に作れるので各々を定義するのは不可能です。もちろん、ジェネリクスを使えばできそうな気はしますがGoでは1.18から入った機能なのでやっぱり定義できません。
一方、int
型はGo言語の中にdefined typeで定義されています。
go/builtin.go at master · golang/go · GitHub
ということでMyInt
型にint
型を代入できないように、MyIntSlice
型に[]int
型を代入できないようにしたくてもできないのでは?という考察でした。
考察おわり!(繰り返しになりますが大いに間違えている可能性があることをご留意ください。)
参考資料
つまづき3:整数リテラルをtype X int型に代入できる
つまづき2で以下はコンパイルエラーになることを示しました。
type MyInt int func returnMyInt(myInt MyInt) MyInt { return myInt } func main() { three := 3 v := returnMyInt(three) // ここでコンパイルエラー fmt.Println(v) }
これはMyInt
型にint
型を代入しようとしていますが代入可能性を満たさないためコンパイルエラーになっているのでした。
しかし以下はコンパイルエラーになりません。
type MyInt int func returnMyInt(myInt MyInt) MyInt { return myInt } func main() { // 変数に入れないで3を直接渡す v := returnMyInt(3) // コンパイルエラーにならない fmt.Println(v) }
なぜこれが通るのか
この例が示しているのはMyInt
型の変数に「3」が代入可能ということですね。まず「3」が何者なのかを見てみます。
整数リテラルとuntyped
「3」というのはGo言語においては整数リテラルと呼びます。整数リテラルについての仕様を見てみます。
The Go Programming Language Specification - The Go Programming Language
An integer literal is a sequence of digits representing an integer constant
整数リテラルとはinteger constantを表現している数字の列のこと、ということみたいですね。そしてconstantといえばtypedだったりuntypedだったりするのでした(https://go.dev/ref/spec#Constants)。今回の場合はuntypedです。
代入可能性
この例は代入可能性のどの条件を満たすのかというと
x
is an untyped constant representable by a value of typeT
.
を満たします。MyInt
型が取る値はint
なので「3」はMyInt
型で表現可能です。先に述べたように「3」という整数リテラルはuntyped constantなのでこの条件を満たすというわけです。
今回は整数リテラルを代入してみましたがconstで定義した場合も同様の理由でコンパイルが通ります。
type MyInt int func returnMyInt(myInt MyInt) MyInt { return myInt } const three = 3 // untyped constant func main() { v := returnMyInt(three) // コンパイルエラーにならない fmt.Println(v) }
参考資料
まとめ
Go言語を使っていて「あれ、これできるんだ」「これはできないんだ」と感じたものを仕様から追ってみました。プログラミング言語の仕様を読むということをしたことがなかったので大変興味深かったです(そして難しかった…先人の記事には大変助けてもらいました!)。Go言語における型がどのようなものなのか理解が深まりました。整数リテラル等のリテラルはその時点では型がなく、それが使われるときに型が確定するというのは驚きでした。
今回は型としてはシンプルな例で、ポインタやインターフェースを絡めた話はなかったのですが思っていたより話が深かったです(そして結構な文量になってしまった…)。
またつまづいたときには仕様も読んでみようかと思います!