はじめに
今月でGolangに触れてからちょうど一年になりますが、未だにGoについて初めて知ることが多々あります。その一つを紹介したいと思います。
以下のGoでなぜキャストが必要なのでしょうか? 当たり前だろ、GoはCなどとは違って明示的なキャストが必要な言語だろとツッコミが入りそうですが、
type INT int
var i1 int
var i2 INT
i1 = 1
i2 = INT(i1) // 明示的なキャストが必要
では、以下はキャスト不要で代入できますが、これはなぜでしょうか? といわれると案外答えられない人もいるのではないでしょうか?
そのあたりをGoの仕様をちゃんと読んで理解しましょうというのが今回のテーマです。
type INTSLICE []int
var l1 []int
var l2 INTSLICE
l1 = []int{1,2,3}
l2 = l1 // 明示的なキャストが不要
仕様をよんでみる
代入可能性(Assignability
)について定義されているのは、https://golang.org/ref/spec#Assignability です。
Assignability
があると関数の引数や返り値に指定できたり、比較演算値でお互いに比較が可能になったりします。他に何ができるようになるか全て知りたければGoの仕様をassignable
でgrepしましょう。
Assignability
の記述を引用します(連番は筆者による)。
A value x is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:
- x's type is identical to T.
- x's type V and T have identical underlying types and at least one of V or T is not a defined type.
- T is an interface type and x implements T.
- x is a bidirectional channel value, T is a channel type, x's type V and T have identical element types, and at least one of V or T is not a defined type.
- x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type.
- x is an untyped constant representable by a value of type T.
こちらをちゃんと理解して、INT
のキャストが必要な理由を把握しましょう。
No.5などは確かにそういえばと思いますね。ポインタを返す関数でnilを返却できたりしますもんね。
No.1をよんで「型の同一性とは?」となりますが、それはすぐ上の
https://golang.org/ref/spec#Type_identity にあります。
抜粋すると、
A defined type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types. In detail:
・ Two array types are identical if they have identical element types and the same array length.
・ Two slice types are identical if they have identical element types.
・ Two struct types are identical if they have the same sequence of fields, and if corresponding fields have the same names, and identical types, and identical tags. Non-exported field names from different packages are always different.
...(省略)
defined type
とかunderlying type
という用語がでてきます。ここではごく一部のみ引用し、説明しますが、正確な定義は仕様をご確認ください。
ざっくり説明すると、新しい型を定義するときの type [defined type] [underlying type]
に該当します(後述の例の型B
やD
のように異なる場合があります。後ほど正確な定義に触れます)。
type INT int
でいうところのdefined type
がINT
、underlying type
がint
です。
defined type
の仕様では、
The new type is called a defined type. It is different from any other type, including the type it is created from.
とあり、INT
とint
は全く別物とみなされます。
少し脱線しますが、
A defined type may have methods associated with it. It does not inherit any methods bound to the given type, but the method set of an interface type or of elements of a composite type remains unchanged:
とあるように、type
ではメソッドは引き継がれません。
underlying type
の仕様では、
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 type declaration.
とあります。predeclared
とはGo言語側が事前に定義してあるint
やstring
などを指します。type literal
とは
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .
です。[]int
などはtype literal
だとわかりますね!
一方、int
、string
などはtype literal
ではないことに注意してください。
type declaration
(型宣言)はtype
による型定義の他、エイリアスによる型宣言の二つ両方を含みます。
注意深く読むとunderlying type
は再起的定義になっていて、
type A int
type B A
type C []B
type D C
の場合、int
、A
、B
のunderlying type
はint
であることや、[]B
、C
、D
のunderlying type
は[]B
であることが導かれます。
ここまで、「型の同一性」「defined type」「underlying type」といった概念が出てきました。これらを踏まえてやっとAssignability
が理解できます。Assignability
の説明に戻る前にここまででわかることを一度整理します。
-
INT
とint
は型が異なる -
INTSLICE
と[]int
は型が異なる -
INT
はdefined type
-
INTSLICE
はdefined type
-
int
のunderlying type
はint
自身 -
INT
のunderlying type
はint
-
[]int
のunderlying type
は[]int
自身 -
INTSLICE
のunderlying type
は[]int
では、Assignability
の定義にもどりましょう。
まず、No.1はINT
、INTSLICE
の両方のケースにあてはまらないことがわかります。
No.2ですが、
x's type V and T have identical underlying types and
この部分は両方のケースであてはまります。INT
のunderlying type
はint
、INTSLICE
のunderlying type
は[]int
だからです。
at least one of V or T is not a defined type.
ここの条件から逆算すると、
-
int
はdefined type
-
[]int
はdefined type
でない
。。。え、?int
がdefined type
だと、、、、
この部分が今回の肝です。10分くらい必死に仕様よんでカラクリに気づくまでかなり混乱しました。
が、わかってしまえば簡単。
都合上、Go言語側で事前にint
、string
などはtype
で型定義しているのです
https://golang.org/ref/spec#Numeric_types には以下にこう書いてます。
To avoid portability issues all numeric types are defined types and thus distinct except byte, which is an alias for uint8, and rune, which is an alias for int32.
なるほどですね。ちなみにbyte
はuint8
のエイリアス、rune
はint32
のエイリアスなので、それらは互いに代入可能です(キャストなしで代入できます)
試しに、Golandでおもむろにint
に対して、「Go to Declartion」すると、
https://github.com/golang/go/blob/be08e10b3bc07f3a4e7b27f44d53d582e15fd6c7/src/builtin/builtin.go#L75
にて、type int int
との定義があります。
これですっきりしたのでした!!
おまけ
今日のテーマは代入可能性でしたが、キャストで型変換可能かはまた別の話で、 https://golang.org/ref/spec#Conversions にあります。
constant等はまた別に細かい定義があるようですが、constant以外では以下の記載があります。
A non-constant value x can be converted to type T in any of these cases:
・x is assignable to T.
・ignoring struct tags (see below), x's type and T have identical underlying types.
・ignoring struct tags (see below), x's type and T are pointer types that are not defined types, and their pointer base types have identical underlying types.
・x's type and T are both integer or floating point types.
・x's type and T are both complex types.
・x is an integer or a slice of bytes or runes and T is a string type.
・x is a string and T is a slice of bytes or runes.
x's type and T have identical underlying types
の部分の条件で、INT
はint
にキャスト可能になるんですね。
型の同一性判定では、struct
のtag
も一致する必要があったのですが、型変換可能性においてはtag
は無視するのが興味深いですね。
最後に今回の総復習として、コードを載せておきます。この挙動が理解できたらカンペキです!!
package main
import "fmt"
type point struct {
x int
y int
}
type point2 struct {
x int
y int
}
type point3 struct {
x int `json:"x"`
y int `json:"y"`
}
func main() {
var p1 = struct {
x int
y int
} {
200,
400,
}
var p2 = struct {
x int
y int
} {
200,
400,
}
fmt.Println(p1==p2) //true t1とt2は型同一かつ値が同じ
p1 = p2 //型同一なのでキャストなしで代入化
var p3 point = p2
var p4 point2 = p2
fmt.Println(p2==p3) // true p2とp3は型同一
// fmt.Println(p3==p4) p3とp4は共にdefined typeなので型が異なる。コメントアウト外すとコンパイルエラー
fmt.Println(p4==point2(p3)) // true 型変換は可能
// var p5 point3 = p2 tagが異なるのでp2とp5は型が異なる。コメントアウト外すとコンパイルエラー
var p5 point3 = point3(p2)
fmt.Println(p5 == point3(p2)) // tagが異なってもキャスト可能
}