The Go Blog

Константы

Роб Пайк
25 августа 2014

Введение

Go — это язык со статической типизацией, который не разрешает операции, в которых смешиваются числовые типы. Вы не можете сложить float64 с int, или даже int32 с int. Тем не менее, допустимо писать 1e6*time.Second или math.Exp(1) или даже 1<<(' '+2.0). В Go константы, в отличие от переменных, ведут себя примерно так же, как обычные числа. Эта запись объясняет, почему так происходит и что это означает.

Фон: C

В ранние дни размышлений о языке Go мы обсуждали ряд проблем, вызванных тем, как C и его потомки позволяют смешивать числовые типы. Множество загадочных ошибок, сбоев и проблем с переносимостью вызваны выражениями, которые объединяют целые числа разных размеров и знаковости. Хотя для опытного программиста на C результат вычисления, например,

<code>unsigned int u = 1e9;
long signed int i = -1;
... i + u ...
</code>

может быть знакомым, он a priori не очевиден. Каков размер результата? Каково его значение? Является ли оно знаковым или беззнаковым?

Здесь та隐藏ятся подвохи.

C имеет набор правил, называемых «обычными арифметическими преобразованиями», и это указывает на их сложность тем, что они со временем изменялись (вводя новые ошибки, которые могут быть обнаружены только после).

При проектировании Go мы решили избежать этой ловушки, запретив смешение числовых типов. Если вы хотите сложить i и u, вы должны быть явны в том, каким должен быть результат. Учитывая

<code>var u uint
var i int
</code>

вы можете написать либо uint(i)+u, либо i+int(u), при этом смысл и тип сложения будут ясны, но, в отличие от C, вы не можете написать i+u. Вы даже не можете смешивать int и int32, даже если int является 32-битным типом.

Эта строгость устраняет частую причину ошибок и других сбоев. Это важное свойство 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 также имеет необработанные строковые литералы, заключённые в обратные кавычки ``, но для целей этого обсуждения они обладают всеми одинаковыми свойствами.) Вот пример строковой константы:

<code>"Hello, 世界"
</code>

(За более подробной информацией о представлении и интерпретации строк см. эту статью в блоге.)

Какой тип имеет эта строковая константа? Очевидный ответ — string, но это неправильно.

Это бестиповая строковая константа, то есть это текстовое значение, которое ещё не имеет фиксированного типа. Да, это строка, но это не значение Go типа string. Оно остаётся бестиповой строковой константой даже после присвоения имени:

<code>const hello = "Hello, 世界"
</code>

После этой декларации hello также является бестиповой строковой константой. Бестиповая константа — это просто значение, ещё не имеющее определённого типа, который заставил бы его следовать строгим правилам, предотвращающим комбинирование значений разных типов.

Это понятие бестиповой константы позволяет нам использовать константы в Go с большой свободой.

Итак, что же такое типизированная строковая константа? Это константа, которой был присвоен тип, например:

<code>const typedHello string = "Hello, 世界"
</code>

Обратите внимание, что объявление typedHello содержит явный тип string перед знаком равенства. Это означает, что typedHello имеет тип string в Go и не может быть присвоена переменной Go другого типа. То есть этот код работает:

<span>
package main
import "fmt"
const typedHello string = "Hello, 世界"
func main() {
  </span>
var s string
s = typedHello
fmt.Println(s)
<span>}
</span>

но это не работает:

<span>
package main
import "fmt"
const typedHello string = "Hello, 世界"
func main() {
  </span>
type MyString string
var m MyString
m = typedHello // Type error
fmt.Println(m)
<span>}
</span>

Переменная m имеет тип MyString и не может быть присвоена значение другого типа. Она может быть присвоена только значениями типа MyString, например так:

<span>
package main
import "fmt"
const typedHello string = "Hello, 世界"
func main() {
  type MyString string
  var m MyString
  </span>
const myStringHello MyString = "Hello, 世界"
m = myStringHello // OK
fmt.Println(m)
<span>}
</span>

или путем принудительного преобразования, например так:

<span>
package main
import "fmt"
const typedHello string = "Hello, 世界"
func main() {
  type MyString string
  var m MyString
  </span>
m = MyString(typedHello)
fmt.Println(m)
<span>}
</span>

Возвращаясь к нашему недженериковому строковому константному значению, у него есть полезное свойство: поскольку у него нет типа, присвоение его типизированной переменной не вызывает ошибку типа. То есть, мы можем написать

<code>m = "Hello, 世界"
</code>

или

<code>m = hello
</code>

потому что, в отличие от типизированных констант typedHello и myStringHello, недженериковые константы "Hello, 世界" и hello не имеют типа. Присвоение их переменной любого типа, совместимого со строками, работает без ошибок.

Эти недженериковые строковые константы, конечно же, являются строками, поэтому они могут использоваться только там, где разрешена строка, но у них нет типа string.

Тип по умолчанию

Как программист на Go, вы, вероятно, уже встречали множество объявлений вроде

<code>str := "Hello, 世界"
</code>

и, возможно, задавались вопросом: «если константа недженериковая, как же str получает тип в этом объявлении переменной?» Ответ заключается в том, что недженериковая константа имеет тип по умолчанию — неявный тип, который она передаёт значению, если тип нужен, но не указан. Для недженериковых строковых констант тип по умолчанию, очевидно, string, поэтому

<code>str := "Hello, 世界"
</code>

или

<code>var str = "Hello, 世界"
</code>

означает именно то же самое, что и

<code>var str string = "Hello, 世界"
</code>

Одним из способов мышления о нетипизированных константах является представление о том, что они существуют в неком идеальном пространстве значений, пространстве менее строгом, чем полная система типов Go. Но чтобы с ними что-то сделать, нужно присвоить их переменным, и когда это происходит, переменная (а не сама константа) должна иметь тип, и константа может сообщить переменной, какой тип она должна иметь. В этом примере str становится значением типа string, потому что нетипизированная строковая константа даёт объявлению тип по умолчанию, string.

В таком объявлении переменная объявляется с типом и начальным значением. Иногда, когда мы используем константу, назначение значения не всегда очевидно. Например, рассмотрим следующую инструкцию:

<span>
package main
import "fmt"
func main() {
  </span>
fmt.Printf("%s", "Hello, 世界")
<span>}
</span>

Сигнатура fmt.Printf выглядит так:

<code>func Printf(format string, a ...interface{}) (n int, err error)
</code>

то есть её аргументы (после строкового формата) являются значениями интерфейса. Что происходит, когда вызывается fmt.Printf с нетипизированной константой, так это создаётся значение интерфейса для передачи в качестве аргумента, и конкретный тип, хранящийся для этого аргумента, — это тип по умолчанию константы. Этот процесс аналогичен тому, что мы видели ранее при объявлении инициализированного значения с использованием нетипизированной строковой константы.

Результат можно увидеть в следующем примере, который использует формат %v для печати значения и %T для печати типа значения, передаваемого в fmt.Printf:

<span>
package main
import "fmt"
const hello = "Hello, 世界"
func main() {
  </span>
fmt.Printf("%T: %v\n", "Hello, 世界", "Hello, 世界")
fmt.Printf("%T: %v\n", hello, hello)
<span>}
</span>

Если константа имеет тип, он передаётся в интерфейс, как показано в этом примере:

<span>
package main
import "fmt"
type MyString string
const myStringHello MyString = "Hello, 世界"
func main() {
  </span>
fmt.Printf("%T: %v\n", myStringHello, myStringHello)
<span>}
</span>

(Для получения дополнительной информации о том, как работают значения интерфейса, см. первые разделы этой статьи в блоге.)

В заключение, типизированная константа подчиняется всем правилам типизированных значений в Go. С другой стороны, нетипизированная константа не несёт тип Go таким же образом и может свободно сочетаться и смешиваться. Однако она имеет тип по умолчанию, который становится доступен тогда и только тогда, когда никакая другая информация о типе не предоставляется.

Тип по умолчанию определяется синтаксисом

Тип по умолчанию для неявной (неприведённой) константы определяется её синтаксисом. Для строковых констант единственным возможным неявным типом является string. Для числовых констант неявный тип имеет больше вариантов. Целочисленные константы по умолчанию имеют тип int, вещественные константы — float64, символьные константы — rune (псевдоним для int32), а мнимые константы — complex128. Вот стандартная инструкция print, которая многократно используется для демонстрации типов по умолчанию:

<span>
package main
import "fmt"
func main() {
  </span>
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)
<span>}
</span>

(Упражнение: объясните результат для 'x'.)

Логические значения

Всё, что было сказано о неявных строковых константах, можно сказать и о неявных логических константах. Значения true и false являются неявными логическими константами, которые могут быть присвоены любой логической переменной, но как только логическая переменная получает тип, её нельзя смешивать с другими типами:

<span>
package main
import "fmt"
func main() {
  </span>
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)
<span>}
</span>

Запустите пример и посмотрите, что произойдёт, а затем закомментируйте строку с «Bad» и снова выполните программу. Шаблон здесь соответствует шаблону строковых констант.

Вещественные числа

Вещественные константы в основном ведут себя так же, как и логические константы. Наш стандартный пример работает ожидаемо:

<span>
package main
import "fmt"
func main() {
  </span>
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)
<span>}
</span>

Одно исключение заключается в том, что в Go существует два типа вещественных чисел: float32 и float64. Тип по умолчанию для вещественной константы — float64, хотя неявная вещественная константа может быть успешно присвоена переменной типа float32:

<span>
package main
import "fmt"
func main() {
  const Zero = 0.0
  const TypedZero float64 = 0.0
  </span>
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

—это просто число, в конце концов, но присвоить его или даже напечатать нельзя. Эта инструкция даже не скомпилируется:

<span>
package main
import "fmt"
func main() {
  const Huge = 1e1000
  </span>
fmt.Println(Huge)
<span>}
</span>

Ошибка: «constant 1.00000e+1000 overflows float64», что является правдой. Но Huge может быть полезной: мы можем использовать её в выражениях с другими константами и получить значение этих выражений, если результат может быть представлен в диапазоне типа float64. Инструкция

<span>
package main
import "fmt"
func main() {
  const Huge = 1e1000
  </span>
fmt.Println(Huge / 1e999)
<span>}
</span>

выводит 10, как и ожидалось.

В связанном с этим смысле числовые константы с плавающей точкой могут иметь очень высокую точность, что делает арифметические операции с ними более точными. Константы, определённые в пакете math, заданы с гораздо большим количеством знаков, чем доступно в типе float64. Вот определение math.Pi:

<code>Pi  = 3.14159265358979323846264338327950288419716939937510582097494459
</code>

Когда это значение присваивается переменной, некоторая точность будет потеряна; присваивание создаст значение типа float64 (или float32), наиболее близкое к высокоточному значению. Этот фрагмент

<span>
package main
import (
  "fmt"
  "math"
)
func main() {
  </span>
pi := math.Pi
fmt.Println(pi)
<span>}
</span>

выводит 3.141592653589793.

Наличие столь большого количества знаков означает, что вычисления, такие как Pi/2 или другие более сложные выражения, могут сохранять большую точность до момента присваивания результата, что делает написание вычислений с константами проще и без потери точности. Это также означает, что в выражениях с константами не возникает особых случаев с плавающей точкой, таких как бесконечности, мягкое переполнение и NaNs. (Деление на константный ноль — это ошибка времени компиляции, и когда всё является числом, не существует понятия «не число».)

Комплексные числа

Комплексные константы ведут себя очень похоже на числа с плавающей точкой. Вот версия нашей знакомой цитаты, переведённой на комплексные числа:

<span>
package main
import "fmt"
func main() {
  </span>
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)
<span>}
</span>

Тип по умолчанию для комплексного числа — complex128, более точная версия, состоящая из двух значений типа float64.

Для ясности в нашем примере мы написали полное выражение (0.0+1.0i), но это значение можно сократить до 0.0+1.0i, 1.0i или даже 1i.

Попробуем немного поиграть. Мы знаем, что в Go числовая константа — это просто число. А что если это число является комплексным числом без мнимой части, то есть вещественным? Вот такой пример:

const Two = 2.0 + 0i

Это не типизированная комплексная константа. Несмотря на то, что у неё нет мнимой части, синтаксис выражения определяет её как имеющую тип по умолчанию complex128. Поэтому, если мы используем её для объявления переменной, тип по умолчанию будет complex128. Фрагмент кода

<span>
package main
import "fmt"
func main() {
  const Two = 2.0 + 0i
  </span>
s := Two
fmt.Printf("%T: %v\n", s, s)
<span>}
</span>

выведет complex128: (2+0i). Но численно Two может быть сохранено в скалярном числе с плавающей точкой, в float64 или float32, без потери информации. Следовательно, можно присвоить Two переменной типа float64, как при инициализации, так и при присваивании, без проблем:

<span>
package main
import "fmt"
func main() {
  const Two = 2.0 + 0i
  </span>
var f float64
var g float64 = Two
f = Two
fmt.Println(f, "and", g)
<span>}
</span>

Вывод будет: 2 and 2. Несмотря на то, что Two является комплексной константой, её можно присвоить переменным с типом плавающей точки. Эта способность константы «пересекать» типы будет полезна.

Целые числа

Наконец мы дошли до целых чисел. У них больше компонентов — разные размеры, знаковые и беззнаковые, и другие — но они следуют тем же правилам. В последний раз, вот наш знакомый пример, использующий только int:

<span>
package main
import "fmt"
func main() {
  </span>
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)
<span>}
</span>

Тот же пример может быть реализован для любого из целочисленных типов, которые следующие:

<code>int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64
uintptr
</code>

(плюс псевдонимы byte для uint8 и rune для int32). Это много, но паттерн работы констант должен быть достаточно знакомым, чтобы вы могли понять, как всё будет происходить.

Как упоминалось выше, целые числа бывают нескольких форм, и каждая форма имеет свой тип по умолчанию: int для простых констант, таких как 123 или 0xFF или -14 и rune для символов в апострофах, таких как ‘a’, ‘世’ или ‘\r’.

Ни одна форма констант не имеет своим типом по умолчанию беззнаковый целочисленный тип. Однако гибкость неопределяемых констант позволяет нам инициализировать беззнаковые целочисленные переменные с помощью простых констант, при условии, что мы чётко указываем тип. Это аналогично тому, как можно инициализировать float64 с помощью комплексного числа с нулевой мнимой частью. Вот несколько различных способов инициализации uint; все они эквивалентны, но все должны явно упоминать тип, чтобы результат был беззнаковым.

<code>var u uint = 17
var u = uint(17)
u := uint(17)
</code>

Аналогично проблеме диапазона, упомянутой в разделе о числах с плавающей точкой, не все целочисленные значения могут поместиться во все целочисленные типы. Могут возникнуть две проблемы: значение может быть слишком большим, или отрицательное значение может быть присвоено беззнаковому целочисленному типу. Например, int8 имеет диапазон от -128 до 127, поэтому константы вне этого диапазона никогда не могут быть присвоены переменной типа int8:

<span>
package main
func main() {
  </span>
var i8 int8 = 128 // Error: too large.
<span>   _ = i8
}
</span>

Аналогично, uint8, также известный как byte, имеет диапазон от 0 до 255, поэтому большое или отрицательное значение не может быть присвоено uint8:

<span>
package main
func main() {
  </span>
var u8 uint8 = -1 // Error: negative value.
<span>   _ = u8
}
</span>

Эта проверка типов может выявить такие ошибки:

<span>
package main
func main() {
  </span>
type Char byte
var c Char = '世' // Error: '世' has value 0x4e16, too large.
<span>   _ = c
}
</span>

Если компилятор сообщает о вашем использовании константы, это, вероятно, реальная ошибка, как в данном случае.

Упражнение: наибольшее беззнаковое целое

Вот небольшое информативное упражнение. Как выразить константу, представляющую наибольшее значение, которое помещается в uint? Если бы речь шла о uint32 вместо uint, мы могли бы написать

<code>const MaxUint32 = 1<<32 - 1
</code>

но мы хотим uint, а не uint32. Типы int и uint имеют равное неопределенное количество бит — либо 32, либо 64. Поскольку количество доступных бит зависит от архитектуры, мы не можем просто записать одно значение.

Любители дополнения до двойки, которое используется в Go для целых чисел, знают, что представление -1 имеет все свои биты, установленные в 1, поэтому внутренне битовая маска -1 совпадает с представлением наибольшего беззнакового целого. Поэтому мы можем подумать, что можем написать

<span>
package main
func main() {
  </span>
const MaxUint uint = -1 // Error: negative value
<span>}
</span>

но это запрещено, потому что -1 не может быть представлен беззнаковой переменной; -1 не находится в диапазоне беззнаковых значений. Преобразование тоже не поможет по той же причине:

<span>
package main
func main() {
  </span>
const MaxUint uint = uint(-1) // Error: negative value
<span>}
</span>

Несмотря на то, что во время выполнения значение -1 может быть преобразовано в беззнаковое целое, правила для константных преобразований запрещают такое преобразование на этапе компиляции. То есть, это работает:

<span>
package main
func main() {
  </span>
var u uint
var v = -1
u = uint(v)
<span>   _ = u
}
</span>

но только потому что v является переменной; если бы мы сделали v константой, даже не типизированной константой, мы бы снова оказались в запрещённой области:

<span>
package main
func main() {
  </span>
var u uint
const v = -1
u = uint(v) // Error: negative value
<span>   _ = u
}
</span>

Мы возвращаемся к предыдущему подходу, но вместо -1 пробуем ^0, побитовое отрицание произвольного количества нулевых бит. Но это тоже не работает по аналогичной причине: В пространстве числовых значений, ^0 представляет бесконечное количество единиц, поэтому если мы присвоим это любому фиксированному целому числу, мы потеряем информацию:

<span>
package main
func main() {
  </span>
const MaxUint uint = ^0 // Error: overflow
<span>}
</span>

Как же тогда представить наибольшее беззнаковое целое число в виде константы?

Ключ в том, чтобы ограничить операцию количеством бит в типе uint и избежать значений, таких как отрицательные числа, которые не могут быть представлены в типе uint. Простейшее значение типа uint — это типизированная константа uint(0). Если у типа uint 32 или 64 бита, то uint(0) будет содержать соответственно 32 или 64 нулевых бита. Если мы инвертируем каждый из этих битов, мы получим правильное количество единичных битов, которое и будет наибольшим значением типа uint.

Следовательно, мы не инвертируем биты для не типизированной константы 0, а инвертируем биты для типизированной константы uint(0). Вот как выглядит наша константа:

<span>
package main
import "fmt"
func main() {
  </span>
const MaxUint = ^uint(0)
fmt.Printf("%x\n", MaxUint)
<span>}
</span>

Независимо от того, сколько бит используется для представления типа uint в текущей среде выполнения (на playground это 32), данная константа корректно представляет наибольшее значение, которое может хранить переменная типа uint.

Если вы понимаете анализ, который привел нас к этому результату, вы понимаете все важные аспекты констант в Go.

Числа

Понятие не типизированных констант в Go означает, что все числовые константы — будь то целые, вещественные, комплексные или даже символьные значения — существуют в некой объединённой области. Только при переходе к вычислительной среде переменных, присваиваний и операций становятся важны конкретные типы. Но пока мы остаёмся в мире числовых констант, мы можем свободно смешивать и сопоставлять значения по желанию. Все эти константы имеют числовое значение 1:

<code>1
1.000
1e3-99.0*10-9
'\x01'
'\u0001'
'b' - 'a'
1.0+3i-3.0i
</code>

Следовательно, несмотря на то, что у них разные неявные типы по умолчанию, записанные как не типизированные константы, они могут быть присвоены переменной любого числового типа:

<span>
package main
import "fmt"
func main() {
  </span>
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)
<span>}
</span>

Вывод этого фрагмента: 1 1 1 1 1 (1+0i) 1.

Вы даже можете делать безумные вещи вроде

<span>
package main
import "fmt"
func main() {
  </span>
var f = 'a' * 1.5
fmt.Println(f)
<span>}
</span>

что даст 145.5, что бесполезно, кроме как доказать определённую мысль.

Но настоящая цель этих правил — гибкость. Эта гибкость означает, что, несмотря на то, что в Go запрещено смешивать переменные с плавающей точкой и целочисленные переменные, или даже int и int32 переменные, в одном выражении, допустимо написать

<code>sqrt2 := math.Sqrt(2)
</code>

или

<code>const millisecond = time.Second/1e3
</code>

или

<code>bigBufferWithHeader := make([]byte, 512+1e6)
</code>

и ожидать от результатов ожидаемого поведения.

Потому что в Go числовые константы ведут себя так, как вы ожидаете: как числа.

Следующая статья: Разворачивание Go-серверов с использованием Docker
Предыдущая статья: Go на OSCON
Индекс блога

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang