ravineport blog

日々の学びをつらつらと

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
}

参考資料

つまづき2:[]inttype 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 type V is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:

  1. V and T are identical.
  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.
  3. V and T are channel types with identical element types, V is a bidirectional channel, and at least one of V or T is not a named type.
  4. T is an interface type, but not a type parameter, and x implements T.
  5. x is the predeclared identifier nil and T is a pointer, function, slice, map, channel, or interface type, but not a type parameter.
  6. x is an untyped constant representable by a value of type T.

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

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 or T is not a named type.

を見てみます。MyIntSlice型はdefined typeなのでnamed typeのひとつです。一方[]int型は型リテラルでnamed typeではありません。

よって

but are not type parameters and at least one of V or T is not a named type

の部分はMyIntSlice型と[]int型については満たしていることがわかります。

では条件の前半部分に戻って

V and T 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 and T have identical underlying types but are not type parameters and at least one of V or T 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

日本語だと「表現可能な」みたいな感じでしょうか。

constant x is representable by a value of type T, where T is not a type parameter, if one of the following conditions applies:

  • x is in the set of values determined by T.
  • T is a floating-point type and x can be rounded to T'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, and x's components real(x) and imag(x) are representable by values of T's component type (float32 or float64).

日本語に雑に訳すと以下のようなかんじでしょうか。

定数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型では代入可能性の以下の条件を満たしたため代入可能でした。

  1. 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.

この条件をMyInt型とint型は満たさないわけですが、差分はどこかというと

at least one of V or T 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 type T.

を満たします。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言語における型がどのようなものなのか理解が深まりました。整数リテラル等のリテラルはその時点では型がなく、それが使われるときに型が確定するというのは驚きでした。

今回は型としてはシンプルな例で、ポインタやインターフェースを絡めた話はなかったのですが思っていたより話が深かったです(そして結構な文量になってしまった…)。

またつまづいたときには仕様も読んでみようかと思います!