The Go Blog
Константы
Введение
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 другого типа.
То есть этот код работает:
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)
Возвращаясь к нашему недженериковому строковому константному значению, у него есть полезное свойство: поскольку у него нет типа, присвоение его типизированной переменной не вызывает ошибку типа. То есть, мы можем написать
<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.
В таком объявлении переменная объявляется с типом и начальным значением. Иногда, когда мы используем константу, назначение значения не всегда очевидно. Например, рассмотрим следующую инструкцию:
fmt.Printf("%s", "Hello, 世界")
Сигнатура fmt.Printf выглядит так:
<code>func Printf(format string, a ...interface{}) (n int, err error)
</code>
то есть её аргументы (после строкового формата) являются значениями интерфейса.
Что происходит, когда вызывается 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)
Одно исключение заключается в том, что в Go существует два типа вещественных чисел: 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)
Ошибка: «constant 1.00000e+1000 overflows float64», что является правдой.
Но Huge может быть полезной: мы можем использовать её в выражениях с другими константами
и получить значение этих выражений, если результат
может быть представлен в диапазоне типа float64.
Инструкция
fmt.Println(Huge / 1e999)
выводит 10, как и ожидалось.
В связанном с этим смысле числовые константы с плавающей точкой могут иметь очень высокую точность,
что делает арифметические операции с ними более точными.
Константы, определённые в пакете math, заданы с гораздо большим количеством знаков, чем доступно в типе float64.
Вот определение math.Pi:
<code>Pi = 3.14159265358979323846264338327950288419716939937510582097494459 </code>
Когда это значение присваивается переменной,
некоторая точность будет потеряна;
присваивание создаст значение типа 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, более точная версия, состоящая из двух значений типа 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 and 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)
Тот же пример может быть реализован для любого из целочисленных типов, которые следующие:
<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:
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?
Если бы речь шла о uint32 вместо uint, мы могли бы написать
<code>const MaxUint32 = 1<<32 - 1 </code>
но мы хотим uint, а не uint32.
Типы int и uint имеют равное неопределенное количество бит — либо 32, либо 64.
Поскольку количество доступных бит зависит от архитектуры, мы не можем просто записать одно значение.
Любители дополнения до двойки,
которое используется в Go для целых чисел, знают, что представление -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 представляет бесконечное количество единиц, поэтому если мы присвоим это любому фиксированному целому числу,
мы потеряем информацию:
const MaxUint uint = ^0 // Error: overflow
Как же тогда представить наибольшее беззнаковое целое число в виде константы?
Ключ в том, чтобы ограничить операцию количеством бит в типе uint и избежать значений, таких как отрицательные числа, которые не могут быть представлены в типе uint. Простейшее значение типа uint — это типизированная константа uint(0). Если у типа uint 32 или 64 бита, то uint(0) будет содержать соответственно 32 или 64 нулевых бита. Если мы инвертируем каждый из этих битов, мы получим правильное количество единичных битов, которое и будет наибольшим значением типа uint.
Следовательно, мы не инвертируем биты для не типизированной константы 0, а инвертируем биты для типизированной константы uint(0). Вот как выглядит наша константа:
const MaxUint = ^uint(0)
fmt.Printf("%x\n", MaxUint)
Независимо от того, сколько бит используется для представления типа 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>
Следовательно, несмотря на то, что у них разные неявные типы по умолчанию, записанные как не типизированные константы, они могут быть присвоены переменной любого числового типа:
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 переменные, в одном выражении, допустимо написать
<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
Индекс блога