The Go Blog
定数
はじめに
Goは静的型付け言語であり、数値型を混ぜる演算は許されません。float64をintに加算したり、int32をintに加算したりすることはできません。しかし、1e6*time.Secondやmath.Exp(1)、さらには1<<(' '+2.0)と書くことは合法です。Goでは、定数は変数とは異なり、ほとんど通常の数値のように振る舞います。この記事では、その理由とそれが何を意味するのかを説明します。
背景: C言語
Goの初期の構想段階で、Cとその派生言語が数値型を自由に混ぜ合わせる方法によって引き起こされる多くの問題について話し合いました。多くの謎のバグ、クラッシュ、移植性の問題は、異なるサイズと「符号付き」の整数を組み合わせる式によって引き起こされます。熟練したCプログラマにとっては、次のような計算結果は
unsigned int u = 1e9;
long signed int i = -1;
... i + u ...
おなじみかもしれませんが、それは自明ではありません。結果はどれくらいの大きさですか?その値は何ですか?符号付きですか、それとも符号なしですか?
ここに厄介なバグが潜んでいます。
Cには「通常の算術変換」と呼ばれる一連のルールがあり、その微妙さを示すのは、長年にわたって変更されてきたことです(これにより、遡ってさらに多くのバグが導入されました)。
Goの設計において、数値型の混在を禁止することで、この地雷原を避けることにしました。iとuを加算したい場合、結果をどうしたいかを明示的に指定する必要があります。次のように与えられた場合、
var u uint
var i int
uint(i)+uまたはi+int(u)のいずれかを記述でき、加算の意味と型が明確に表現されますが、Cとは異なりi+uとは記述できません。intが32ビット型の場合でも、intとint32を混在させることさえできません。
この厳格さは、バグやその他の障害の一般的な原因を排除します。これはGoの重要な特性です。しかし、それにはコストがかかります。プログラマは、意味を明確に表現するために、厄介な数値変換でコードを装飾する必要がある場合があります。
そして、定数についてはどうでしょうか?上記の宣言を考えると、i = 0またはu = 0と書くことを合法にするにはどうすればよいでしょうか?0の型は何でしょうか?i = int(0)のような単純なコンテキストで定数に型変換を要求するのは不合理でしょう。
すぐに、その答えが、数値定数を他のCライクな言語での動作とは異なる方法で動作させることにあると気づきました。多くの考察と実験の後、私たちは、ほとんど常に適切だと感じるデザインを考案しました。これにより、プログラマは常に定数を変換する必要がなく、コンパイラに叱られることなくmath.Sqrt(2)のようなものを書くことができます。
要するに、Goの定数はほとんどの場合、ただ動作します。それがどのように起こるか見てみましょう。
用語
まず、簡単な定義から。Goでは、constは2や3.14159や"scrumptious"のようなスカラー値に名前を付けるキーワードです。そのような値は、名前が付けられていてもいなくても、Goでは定数と呼ばれます。定数は、2+3や2+3iやmath.Pi/2や("go"+"pher")のように、定数から構築された式によっても作成できます。
定数を持たない言語もありますし、定数またはconstという単語のより一般的な定義を持つ言語もあります。たとえばCやC++では、constはより複雑な値のより複雑なプロパティを符号化できる型修飾子です。
しかし、Goでは、定数は単に単純で不変の値であり、ここからはGoのことだけを話します。
文字列定数
数値定数には多くの種類があります—整数、浮動小数点数、ルーン、符号付き、符号なし、虚数、複素数—そこで、より単純な形式の定数である文字列から始めましょう。文字列定数は理解しやすく、Goの定数の型問題を探索するためのより小さな空間を提供します。
文字列定数は、二重引用符で囲まれたテキストです。(Goには、バッククォート``で囲まれた生文字列リテラルもありますが、この議論の目的では、それらはすべて同じプロパティを持っています。)これは文字列定数です
"Hello, 世界"
(文字列の表現と解釈の詳細については、こちらのブログ記事をご覧ください。)
この文字列定数にはどのような型がありますか?明らかな答えはstringですが、それは間違っています。
これは型なし文字列定数であり、つまり、まだ固定された型を持たない定数テキスト値です。はい、それは文字列ですが、Goの型stringの値ではありません。名前が付けられても型なし文字列定数のままです
const hello = "Hello, 世界"
この宣言の後、helloも型なし文字列定数です。型なし定数は単なる値であり、異なる型を持つ値の組み合わせを防ぐ厳格なルールに従うことを強制する定義済み型がまだ与えられていません。
この型なし定数という概念が、Goで定数を非常に自由に使えるようにするものです。
では、型付き文字列定数とは何でしょうか?それは、次のように型が与えられたものです
const typedHello string = "Hello, 世界"
typedHelloの宣言には、等号の前に明示的なstring型があることに注意してください。これは、typedHelloがGoの型stringを持ち、異なる型のGo変数に代入できないことを意味します。つまり、このコードは機能します
var s string
s = typedHello
fmt.Println(s)
しかし、これは機能しません
type MyString string
var m MyString
m = typedHello // Type error
fmt.Println(m)
変数mは型MyStringを持ち、異なる型の値を代入できません。次のように、型MyStringの値のみを代入できます
const myStringHello MyString = "Hello, 世界"
m = myStringHello // OK
fmt.Println(m)
または、次のように変換を強制することで
m = MyString(typedHello)
fmt.Println(m)
型なし文字列定数に戻ると、型がないため、型付き変数に代入しても型エラーが発生しないという便利な特性があります。つまり、次のように書くことができます
m = "Hello, 世界"
または
m = hello
なぜなら、型付き定数typedHelloとmyStringHelloとは異なり、型なし定数"Hello, 世界"とhelloは型を持たないからです。文字列と互換性のある任意の型の変数に代入してもエラーは発生しません。
これらの型なし文字列定数はもちろん文字列なので、文字列が許可される場所でのみ使用できますが、型stringは持ちません。
デフォルトの型
Goプログラマであれば、次のような多くの宣言を確実に見たことがあるでしょう
str := "Hello, 世界"
そして今、「定数が型なしの場合、この変数宣言でstrはどのように型を得るのか?」と尋ねているかもしれません。答えは、型なし定数にはデフォルトの型があるということです。これは、型が提供されていない場合に型が必要なときに、その型を値に転送する暗黙の型です。型なし文字列定数の場合、そのデフォルトの型は明らかにstringなので、
str := "Hello, 世界"
または
var str = "Hello, 世界"
は、次とまったく同じ意味です
var str string = "Hello, 世界"
型なし定数を考える一つの方法は、それらがGoの完全な型システムよりも制限の少ない、ある種の理想的な値の空間に存在するというものです。しかし、それらを使って何かをするには、変数に代入する必要があります。そのとき、変数(定数自体ではなく)が型を必要とし、定数は変数にどのような型を持つべきかを伝えることができます。この例では、型なし文字列定数が宣言にデフォルトの型であるstringを与えるため、strは型stringの値になります。
そのような宣言では、変数は型と初期値で宣言されます。しかし、定数を使用する際、値の宛先がそれほど明確ではない場合があります。たとえば、次のステートメントを考えてみましょう。
fmt.Printf("%s", "Hello, 世界")
fmt.Printfのシグネチャは次のとおりです
func Printf(format string, a ...interface{}) (n int, err error)
つまり、その引数(書式指定文字列の後)はインターフェース値です。fmt.Printfが型なし定数で呼び出されると、引数として渡すためにインターフェース値が作成され、その引数に格納される具体的な型は定数のデフォルトの型になります。このプロセスは、型なし文字列定数を使用して初期化された値を宣言したときに見たものと類似しています。
この例で結果を見ることができます。%vフォーマットを使用して値を出力し、%Tフォーマットを使用してfmt.Printfに渡される値の型を出力します。
fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界")
fmt.Printf("%T: %v\n", hello, hello)
定数が型を持っている場合、その型がインターフェースに入ります。この例が示すように、
fmt.Printf("%T: %v\n", myStringHello, myStringHello)
(インターフェース値の仕組みの詳細については、こちらのブログ記事の最初のセクションをご覧ください。)
まとめると、型付き定数はGoの型付き値のすべてのルールに従います。一方、型なし定数はGoの型を同じ方法で保持せず、より自由に混ぜ合わせることができます。ただし、他の型情報が利用できない場合にのみ公開されるデフォルトの型を持ちます。
構文によって決定されるデフォルトの型
型なし定数のデフォルトの型は、その構文によって決定されます。文字列定数の場合、唯一可能な暗黙の型はstringです。数値定数の場合、暗黙の型はより多様です。整数定数はデフォルトでint、浮動小数点定数はfloat64、ルーン定数はrune(int32のエイリアス)、虚数定数はcomplex128になります。以下に、デフォルトの型の動作を示すために繰り返し使用される典型的なprintステートメントを示します。
fmt.Printf("%T %v\n", 0, 0)
fmt.Printf("%T %v\n", 0.0, 0.0)
fmt.Printf("%T %v\n", 'x', 'x')
fmt.Printf("%T %v\n", 0i, 0i)
(演習: 'x'の結果を説明してください。)
ブール値
型なし文字列定数について述べたことはすべて、型なしブール定数にも当てはまります。値trueとfalseは、任意のブール変数に代入できる型なしブール定数ですが、型が与えられると、ブール変数は混在できません
type MyBool bool
const True = true
const TypedTrue bool = true
var mb MyBool
mb = true // OK
mb = True // OK
mb = TypedTrue // Bad
fmt.Println(mb)
例を実行して何が起こるかを確認し、「Bad」行をコメントアウトして再度実行してください。ここでのパターンは、文字列定数のパターンとまったく同じです。
浮動小数点数
浮動小数点定数は、ほとんどの点でブール定数と似ています。私たちの標準的な例は、変換で期待どおりに機能します
type MyFloat64 float64
const Zero = 0.0
const TypedZero float64 = 0.0
var mf MyFloat64
mf = 0.0 // OK
mf = Zero // OK
mf = TypedZero // Bad
fmt.Println(mf)
1つの問題は、Goにはfloat32とfloat64という2つの浮動小数点型があることです。浮動小数点定数のデフォルトの型はfloat64ですが、型なし浮動小数点定数をfloat32値に代入しても問題ありません。
var f32 float32
f32 = 0.0
f32 = Zero // OK: Zero is untyped
f32 = TypedZero // Bad: TypedZero is float64 not float32.
fmt.Println(f32)
浮動小数点値は、オーバーフロー、つまり値の範囲の概念を導入するのに適した場所です。
数値定数は、任意精度数値空間に存在します。それらは単なる通常の数値です。しかし、変数に代入されると、その値は宛先に収まる必要があります。非常に大きな値を持つ定数を宣言できます
const Huge = 1e1000
—結局のところ、それは単なる数値ですが、それを代入したり、印刷したりすることさえできません。このステートメントはコンパイルすらされません
fmt.Println(Huge)
エラーは「constant 1.00000e+1000 overflows float64」であり、これは正しいです。しかし、Hugeは役に立つかもしれません。他の定数との式で使用し、結果がfloat64の範囲で表現できる場合、それらの式の値を使用できます。次のステートメントは、
fmt.Println(Huge / 1e999)
期待通り10を出力します。
関連して、浮動小数点定数は非常に高い精度を持つことができるため、それらを含む算術演算はより正確になります。mathパッケージで定義されている定数は、float64で利用可能な桁数よりもはるかに多くの桁数で与えられます。以下はmath.Piの定義です。
Pi = 3.14159265358979323846264338327950288419716939937510582097494459
その値が変数に代入されると、精度の一部が失われます。代入は、高精度値に最も近いfloat64(またはfloat32)値を作成します。このスニペットは、
pi := math.Pi
fmt.Println(pi)
3.141592653589793を出力します。
多くの桁数が利用できるということは、Pi/2のような計算やその他のより複雑な評価が、結果が代入されるまでより多くの精度を保持できることを意味し、定数を含む計算を精度を失うことなく簡単に記述できます。また、定数式で無限大、ソフトアンダーフロー、NaNsのような浮動小数点の例外的なケースが発生することはありません。(定数ゼロによる除算はコンパイル時エラーであり、すべてが数値である場合、「数値ではない」というものは存在しません。)
複素数
複素数定数は、浮動小数点定数と非常によく似た動作をします。以下に、おなじみの定型文を複素数に翻訳したバージョンを示します。
type MyComplex128 complex128
const I = (0.0 + 1.0i)
const TypedI complex128 = (0.0 + 1.0i)
var mc MyComplex128
mc = (0.0 + 1.0i) // OK
mc = I // OK
mc = TypedI // Bad
fmt.Println(mc)
複素数のデフォルト型はcomplex128であり、2つのfloat64値で構成されるより高精度のバージョンです。
例を明確にするために、完全な式(0.0+1.0i)と書きましたが、この値は0.0+1.0i、1.0i、さらには1iに短縮できます。
トリックを使いましょう。Goでは、数値定数は単なる数値であることを知っています。その数値が虚数部を持たない複素数、つまり実数である場合はどうなるでしょうか?ここに1つあります。
const Two = 2.0 + 0i
それは型なし複素数定数です。虚数部がなくても、式の構文によってデフォルトの型はcomplex128と定義されます。したがって、それを使用して変数を宣言すると、デフォルトの型はcomplex128になります。次のスニペットは、
s := Two
fmt.Printf("%T: %v\n", s, s)
complex128: (2+0i)を出力します。しかし、数値的には、Twoは情報損失なしにスカラー浮動小数点数、float64またはfloat32に格納できます。したがって、Twoをfloat64に、初期化または代入のいずれかで、問題なく代入できます。
var f float64
var g float64 = Two
f = Two
fmt.Println(f, "and", g)
出力は2 と 2です。Twoは複素数定数ですが、スカラー浮動小数点変数に代入できます。このように定数が型を「横断」できることは、役に立つでしょう。
整数
ついに整数にたどり着きました。それらはより多くの可動部分を持っています—多くのサイズ、符号付きまたは符号なし、その他—しかし、同じルールに従います。最後に、おなじみの例を、今回はintだけを使って示します。
type MyInt int
const Three = 3
const TypedThree int = 3
var mi MyInt
mi = 3 // OK
mi = Three // OK
mi = TypedThree // Bad
fmt.Println(mi)
同じ例は、次のいずれかの整数型で構築できます。
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr
(さらに、uint8のエイリアスであるbyteと、int32のエイリアスであるrune)。たくさんありますが、定数の動作パターンは、ここまでで十分に理解できるでしょう。
前述の通り、整数にはいくつかの形式があり、それぞれの形式には独自のデフォルト型があります。たとえば、123や0xFFや-14のような単純な定数にはint、'a'、'世'、'\r'のような引用符付き文字にはruneが使われます。
デフォルトの型として符号なし整数型を持つ定数形式はありません。しかし、型なし定数の柔軟性により、型を明確にする限り、単純な定数を使用して符号なし整数変数を初期化できます。これは、虚数部がゼロの複素数を使用してfloat64を初期化する方法と類似しています。以下はuintを初期化するいくつかの異なる方法です。すべて同等ですが、結果が符号なしになるには、すべて型を明示的に指定する必要があります。
var u uint = 17
var u = uint(17)
u := uint(17)
浮動小数点値のセクションで述べた範囲の問題と同様に、すべての整数値がすべての整数型に収まるわけではありません。発生する可能性のある問題は2つあります。値が大きすぎるか、負の値が符号なし整数型に代入されていることです。たとえば、int8の範囲は-128から127なので、その範囲外の定数はint8型の変数に代入できません。
var i8 int8 = 128 // Error: too large.
同様に、uint8(byteとしても知られている)の範囲は0から255なので、大きいまたは負の定数をuint8に代入することはできません。
var u8 uint8 = -1 // Error: negative value.
この型チェックは、次のような間違いを捕捉できます。
type Char byte
var c Char = '世' // Error: '世' has value 0x4e16, too large.
コンパイラが定数の使用について文句を言う場合、それはこのような本当のバグである可能性が高いです。
演習: 最大の符号なし整数
ここに有益な小さな演習があります。uintに収まる最大値を表す定数をどのように表現するでしょうか?uintではなくuint32について話しているのであれば、次のように書くことができます。
const MaxUint32 = 1<<32 - 1
しかし、私たちはuint32ではなくuintを望んでいます。intとuintの型は、32ビットまたは64ビットのいずれかの指定されていないビット数を持ちます。利用可能なビット数はアーキテクチャに依存するため、単一の値を書き下すことはできません。
Goの整数が使用するように定義されている2の補数演算のファンは、-1の表現がすべてのビットを1に設定していることを知っているので、-1のビットパターンは内部的に最大の符号なし整数と同じです。したがって、次のように書けると思うかもしれません。
const MaxUint uint = -1 // Error: negative value
しかし、それは違法です。なぜなら、-1は符号なし変数で表現できないからです。-1は符号なし値の範囲に含まれていません。同じ理由で変換も役に立ちません。
const MaxUint uint = uint(-1) // Error: negative value
実行時に-1の値が符号なし整数に変換できるとしても、定数変換のルールは、コンパイル時にこのような強制を禁止しています。つまり、これは機能します。
var u uint
var v = -1
u = uint(v)
しかし、それはvが変数であるためです。vを定数、たとえ型なし定数にしても、禁止された領域に戻ってしまいます。
var u uint
const v = -1
u = uint(v) // Error: negative value
以前のアプローチに戻り、-1の代わりに^0、つまりゼロの任意数のビットのビット反転を試します。しかし、これも同様の理由で失敗します。数値空間では、^0は無限個の1を表すため、これを固定サイズの整数に代入すると情報が失われます。
const MaxUint uint = ^0 // Error: overflow
では、最大の符号なし整数を定数としてどのように表現するのでしょうか?
重要なのは、演算をuintのビット数に制限し、負の数など、uintで表現できない値を避けることです。最も単純なuint値は、型付き定数uint(0)です。uintが32ビットまたは64ビットの場合、uint(0)はそれぞれ32または64のゼロビットを持ちます。これらのビットをそれぞれ反転すると、正しい数の1ビットが得られ、これが最大のuint値になります。
したがって、型なし定数0のビットを反転するのではなく、型付き定数uint(0)のビットを反転します。これが私たちの定数です。
const MaxUint = ^uint(0)
fmt.Printf("%x\n", MaxUint)
現在の実行環境でuintを表すのに必要なビット数がいくつであっても(プレイグラウンドでは32です)、この定数はuint型の変数が保持できる最大値を正しく表します。
この結果にたどり着いた分析を理解できれば、Goにおける定数に関するすべての重要な点を理解できたことになります。
数値
Goにおける型なし定数の概念は、整数、浮動小数点数、複素数、さらには文字値のいずれであっても、すべての数値定数が一種の統一された空間に存在することを意味します。それらを変数、代入、演算という計算の世界に持ち込んだときに、実際の型が重要になります。しかし、数値定数の世界にとどまる限り、好きなように値を混ぜ合わせることができます。これらの定数はすべて数値1を持ちます。
1
1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'b' - 'a'
1.0+3i-3.0i
したがって、それらは異なる暗黙のデフォルト型を持っていますが、型なし定数として記述されている場合、任意の数値型の変数に代入できます。
var f float32 = 1
var i int = 1.000
var u uint32 = 1e3 - 99.0*10.0 - 9
var c float64 = '\x01'
var p uintptr = '\u0001'
var r complex64 = 'b' - 'a'
var b byte = 1.0 + 3i - 3.0i
fmt.Println(f, i, u, c, p, r, b)
このスニペットの出力は次のとおりです。1 1 1 1 1 (1+0i) 1。
次のような奇妙なことさえできます。
var f = 'a' * 1.5
fmt.Println(f)
これは145.5を生成します。これは、論点を証明するため以外には無意味です。
しかし、これらのルールの真のポイントは柔軟性です。その柔軟性とは、Goでは同じ式で浮動小数点変数と整数変数、あるいはintとint32変数を混ぜることは違法であるにもかかわらず、次のように記述しても問題ないということです。
sqrt2 := math.Sqrt(2)
または
const millisecond = time.Second/1e3
または
bigBufferWithHeader := make([]byte, 512+1e6)
そして、結果が期待通りに機能することを意味します。
Goでは、数値定数は期待通りに機能するからです。つまり、数値のように機能します。
次の記事: Docker を使った Go サーバーのデプロイ
前の記事: OSCON での Go
ブログインデックス