🍎

[Go] 言語仕様書の「型と値の特性」を理解する

2021/12/07に公開約6,600字

この記事はCyberAgent 22 新卒 Advent Calendar 2021 8日目の記事です。

https://go.dev/ref/spec

Goの言語仕様書を読む中で

  • Type Identity、Assignability、Representabilityの性質の関係
  • representableだと何が嬉しいのか

などの点が言語仕様書からはわかりづらいと感じたので、整理しようと試みたのがこの記事です。

忙しい人のためのまとめ

Goの型と値の特性(Properties of types and values)は3つある

  1. Type Identity 型が同じ(identical)かどうか
  2. Assignability 型に代入可能(assinable)かどうか
  3. Representability 型に表現可能(representable)かどうか

各性質を満たすと行える操作をまとめると次のようになる。

二項演算が可能 代入が可能
identical
assignable ×
representable ×

Representabilityは定数についてのAssignabilityを定義したものである。


以下ではxを型Tの変数、yは型Vの変数とします。

var x T
var y V

また、以下で登場する用語が分からない場合、別記事にまとめたのでご覧ください。

https://zenn.dev/senk/articles/91fa080844bb12

Type identity

Goの2つの型TVが同じ(identical)なら、==等の二項演算が可能になります。
以下の条件を満たす必要があります。

  1. TVdefined typeではない。
  2. TVunderlying typeが同じである。

例えば次のようにtype definitionされていた場合、

type A int 
type B = int

次の表のようになります。

1. TかVがdefined typeではない 2. TとVはunderlying typeが同じ TとVはidenticalか
Aとint X (Aもintもdefined type) O X
Bとint O (Bはdefined typeではない) O O
intと[]int O ([]intはdefined typeではない) X X

複合型で「TVunderlying typeが同じである。」の判定を行うためには、次にあげる点が一致しないといけません。

配列型

  • 要素の型
  • 配列長

ポインタ型、スライス型

  • 要素の型

チャネル型

  • 要素の型
  • 送受信の方向

構造体型

  • フィールドの並び、型、名前、タグ

インターフェース型

  • method set

関数型

  • 引数の型と個数[1]
  • 返り値の型と個数
  • 可変長かどうか

次はその例です。

[5]int,     [5]int                              // 同じ
chan<- int, chan<- int                          // 同じ
chan int,   chan<- int                          // 違う
*string,    *int                                // 違う
[]string,   []int                               // 違う
func(x int,y int) int, func(z int,w int) int    // 同じ
func(x int,y int) int, func(z int,w int) string // 違う

次の点には注意です。

  • 関数の引数や返り値の名前は一致しなくてもいいが、構造体のフィールドは名前が一致しなければならない。
  • チャネルの長さは一致しなくてもいいが、配列の長さは一致しなければならない。

構造体とインターフェースがidenticalかどうかの挙動は、以下のようになります

package main

func main() {
	a := struct {
		x int
		y int
	}{}
	b := struct {
		x int
		y int
	}{}
	c := struct {
		y int
		x int
	}{}
	d := struct {
		x int
		z int
	}{}
	e := struct {
		x int
		y int `json:"b"`
	}{}
	_ = a == b // OK
	_ = a == c // NG
	_ = a == d // NG
	_ = a == z // NG
}
package main

var a interface {
	hoge(x int, y string)
}

var b interface {
	hoge(y int, x string)
}

var c interface {
	foo(x int)
}

var d interface {
	hoge(x string, y int)
}

func main() {
	_ = a == b // OK
	_ = a == c // NG
	_ = a == d // NG
}

部分的にエクスポートされていない構造体とインターフェース

ここまでの条件以外にもidenticalの条件があります。別のパッケージで宣言されていてエクスポートされていないフィールドのある構造体とインターフェースを利用する場合、その挙動の違いに注意する必要があります。

  • 構造体の場合、どの構造体ともidenticalにはならない。
  • インターフェースの場合、エクスポートされているmethod setが一致しているインターフェースと空のインターフェース(interface{})とidenticalになる。

次はその例です。例は以下のリポジトリに置いておきました。

https://github.com/senk8/examples/tree/master/golang/non_export

まず別のパッケージnon_exportにおいてエクスポートされていないフィールドのある構造体とインターフェース型の変数とを定義します。

package non_export

var NonExportStruct struct {
	Foo  int
	hoge string
}
package non_export

var NonExportInterface interface {
	Foo()
	hoge()
}

次にpackage mainから2つの変数をインポートして比較します。

package main

import "non_export/non_export"

func main() {
	var a struct {
		Foo int
	}
	var b struct {
		Foo  int
		hoge int
	}
	var c interface {
		Foo()
		hoge()
	}
	var d interface {
		Foo()
	}
	var e interface{}
	_ = a == non_export.NonExportStruct    // NG
	_ = b == non_export.NonExportStruct    // NG
	_ = c == non_export.NonExportInterface // NG
	_ = d == non_export.NonExportInterface // OK
	_ = e == non_export.NonExportInterface // OK
}

このように構造体とインターフェースにおいて異なる挙動をすることがわかります。

Assignability

xyに代入可能(assignable)なら、次のコンパイルが可能です。

y = x 

xの型Tyの型Videnticalなら代入可能です。assignableidenticalより制約が弱く、次の場合でもassignableです。

  1. xnilVが複合型の場合
package main

func main() {
	var (
		a []string
		b int
	)
	a = nil // OK
	b = nil // NG
}
  1. TVunderlying typeが同じかつ、TVのうち1つ以上がdefinedでない
package main

func main() {
	type (
		A []string
		B []string
	)
	var (
		a []string
		b A
		c B
	)
	a = []string{"hoge"} // OK
	b = a                // OK
	a = 1                // NG
	c = b                // NG
}
  1. TVを実装している
package main

import "io"

func main() {
	var (
		a interface{}
		b io.Reader
	)
	a = 1 // OK
	b = 1 // NG
}
  1. xが双方向のチャネルとすると、Vがチャネルで、VTの要素型が同じで、TとVの1つ以上がdefined typeでない
package main

func main() {
	type A chan<- int
	var (
		a chan int
		b chan<- int
		c A
		d chan string
	)
	a = make(chan int)
	b = a // OK
	c = a // OK
	a = b // NG
	d = a // NG
}

Representability

Assignabilityの条件は上記だけでなく

  • xuntyped constantである時、xTとしてrepresentableである

という条件があります。representableとは定数x

  • Tの表現できる範囲に含まれる。
  • x が浮動小数点定数なら、xはオーバフローなく型Tに丸められる。
  • x が複素数定数なら、real(x)imag(x)Tの成分型で表現できる。

整数としてrepresentableならAssignableであることに加えて、シフト演算や配列のインデックスやmakeなどに指定できます[2]

このようにrepresentableは定数がassignableかどうかを定義した概念だと言えます。次は定数の型変換を用いたrepresentableかどうかの例です。

func main() {
	var (
		a uint8
		b float32
		c string
	)
	a = 255         // OK
	a = 1.0         // OK
	a = 256         // NG
	a = 1.000000001 // NG
	b = 12.0        // OK
	b = 12          // OK
	b = 99 + 0i     // OK
	b = 3.4e39      // NG
	b = 0 + 99i     // NG
	b = 1e100000    // NG
	c = 'a'         // NG
}

representableと似たような概念としてconvertable[3]があります。例えば、ここ
を確認すると、Identicalassinableはありますが、representableはなく、代わりにconvertable*となっています。

記事内容に誤りがあった場合、コメントで教えていただけると嬉しいです。

参考にさせていただいた記事

https://go.dev/ref/spec
https://zenn.dev/hsaki/articles/gospecdictionary
https://zenn.dev/nobishii/articles/defined_types
https://qiita.com/behiron/items/89bf7292aec48b097fe4
https://zenn.dev/syumai/articles/77bc12aca9b654
脚注
  1. 引数や返り値が2個以上あり、型の順番が異なる場合、違う型として見なされます。 ↩︎

  2. ここを見ると、整数引数として代入されているので実際にはAssignableと同じことを言っています。 ↩︎

  3. 型変換に関する詳細はをここを参考にしてください。https://go.dev/ref/spec#Conversions ↩︎

Discussion

ログインするとコメントできます