Go ブログ
定数
はじめに
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, 世界"
型のない定数について考える1つの方法は、それらが一種の理想的な値の空間に存在するということです。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 には*2つ*の浮動小数点型があることです。 float32
と float64
です。浮動小数点定数のデフォルト型は 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)
エラーは、「定数 1.00000e+1000 は 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
やその他のより複雑な評価のような計算が、結果が代入されるまでより高い精度を維持できることを意味し、定数を伴う計算の精度を落とすことなく記述しやすくなります。また、定数式で無限大、ソフトアンダーフロー、NaN
などの浮動小数点のコーナーケースが発生する状況がないことを意味します。(定数のゼロによる除算はコンパイル時エラーであり、すべてが数値である場合、「数値ではない」ということはありません。)
複素数
複素数定数は、浮動小数点定数と非常によく似た動作をします。おなじみの定数の複素数版を以下に示します。
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 では、数値定数は単なる数値であることが分かっています。その数値が虚数部のない複素数、つまり実数である場合はどうなるでしょうか?以下に例を示します。
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.
同様に、byte
とも呼ばれる uint8
の範囲は 0 から 255 であるため、大きな定数または負の定数を uint8
に代入することはできません。
var u8 uint8 = -1 // Error: negative value.
この型チェックにより、次のようなミスを検出できます。
type Char byte var c Char = '世' // Error: '世' has value 0x4e16, too large.
コンパイラが定数の使用について文句を言う場合は、このような実際のバグの可能性があります。
演習:最大の符号なし int
有益なちょっとした演習をしてみましょう。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)
現在の Ausführungsumgebung で uint
を表すのに必要なビット数に関係なく(playground では 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
ブログインデックス