Типы
В предыдущем разделе о типах данных мы уже кратко рассмотрели все встроенные типы данных в Go. Эти встроенные базовые типы являются основой для пользовательских типов. Go является типичным статически типизированным языком, где тип всех переменных определяется во время компиляции и не изменяется на протяжении всего жизненного цикла программы. В этом разделе мы кратко рассмотрим систему типов Go и основы их использования.
Статическая строгая типизация
Go — это язык со статической строгой типизацией. Статическая означает, что типы всех переменных в Go определяются ещё во время компиляции и не изменяются на протяжении всего жизненного цикла программы. Хотя краткое объявление переменных в Go немного напоминает стиль динамических языков, тип переменных выводится компилятором. Основное различие заключается в том, что тип, будучи выведенным, больше не изменяется, тогда как в динамических языках всё наоборот. Поэтому следующий код не пройдёт компиляцию, поскольку переменная a имеет тип int и не может быть присвоена строка.
func main() {
var a int = 64
a = "64"
fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}Строгая типизация означает, что в программе выполняется строгая проверка типов. При возникновении несоответствия типов программа немедленно сообщает программисту об ошибке, вместо того чтобы пытаться вывести возможный результат, как это делают динамические языки. Поэтому следующий код не пройдёт компиляцию, поскольку типы не совпадают и не могут быть выполнены операции.
func main() {
fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}Объявление типов после имени
Почему в Go объявление типа ставится после имени, а не перед ним? Во многом это связано с уроками, извлечёнными из C. Рассмотрим пример из официальной документации — это указатель на функцию:
int (*(*fp)(int (*)(int, int), int))(int, int)Честно говоря, даже при внимательном рассмотрении трудно понять, что это за тип. В Go аналогичная запись выглядит следующим образом:
f func(func(int,int) int, int) func(int, int) intСтиль объявления в Go всегда следует принципу: имя впереди, тип после. Читая слева направо, можно сразу понять, что это функция, и что возвращаемое значение имеет тип func(int,int) int. Когда типы становятся более сложными, объявление типа после имени значительно улучшает читаемость. Многие аспекты дизайна Go служат для улучшения читаемости.
Объявление типа
В Go с помощью объявления типа можно объявить новый тип с пользовательским именем. Объявление нового типа обычно требует имени типа и базового типа. Простой пример:
type MyInt int64В приведённом объявлении типа с помощью ключевого слова type объявлен тип с именем MyInt и базовым типом int64. В Go каждый объявленный тип должен иметь соответствующий базовый тип, и не рекомендуется использовать имена типов, совпадающие с уже существующими встроенными идентификаторами.
type MyInt int64
type MyFloat64 float64
type MyMap map[string]int
// Можно скомпилировать, но не рекомендуется использовать, так как это перекрывает существующий тип
type int int64Типы, объявленные через объявление типа, являются новыми типами. Различные типы не могут участвовать в операциях, даже если их базовые типы одинаковы.
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)invalid operation: f1 + f (mismatched types MyFloat64 and float64)Псевдоним типа
Псевдоним типа отличается от объявления типа. Псевдоним типа — это просто алиас, а не создание нового типа. Простой пример:
type Int = intОба являются одним и тем же типом, отличаются только названиями, поэтому могут участвовать в операциях. Следовательно, следующий код пройдёт компиляцию.
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)3Псевдонимы типов особенно полезны для сложных типов. Например, есть тип map[string]map[string]int — это двумерная карта. Теперь есть функция, параметр которой имеет тип map[string]map[string]int:
func PrintMyMap(mymap map[string]map[string]int) {
fmt.Println(mymap)
}В этом случае нет необходимости использовать объявление типа, поскольку оно создаёт новый тип, который не может быть использован в качестве параметра функции. Пример с использованием псевдонима типа:
type TwoDMap = map[string]map[string]int
func PrintMyMap(mymap TwoDMap) {
fmt.Println(mymap)
}Использование псевдонима типа делает код более лаконичным.
TIP
Встроенный тип any является псевдонимом типа interface{}, они полностью эквивалентны и отличаются только названием.
Преобразование типов
В Go существуют только явные преобразования типов, не существует неявных преобразований типов, поэтому переменные разных типов не могут участвовать в операциях и не могут передаваться в качестве параметров. Преобразование типов применимо при условии, что известен тип преобразуемой переменной и целевой тип. Пример:
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)0.30000000000000004Только после явного преобразования MyFloat64 в тип float64 можно выполнить операцию сложения. Другим условием преобразования типов является то, что преобразуемый тип должен быть представимым (Representability) целевым типом. Например, int может быть представлен типом int64, а также типом float64, поэтому между ними возможно явное преобразование типов. Однако int не может быть представлен типами string и bool, поэтому преобразование между ними невозможно.
TIP
Определение представимости (Representability) можно найти в справочнике — Representability для получения более подробной информации.
Даже если два типа могут представлять друг друга, результат преобразования типов не всегда корректен. Рассмотрим следующий пример:
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))1 0num1 был корректно преобразован в тип int32, но num2 — нет. Это типичная проблема переполнения. int32 может представлять 31-битные целые числа, int8 — только 7-битные. При преобразовании целых чисел большей точности в целые числа меньшей точности старшие биты отбрасываются, а младшие сохраняются, поэтому результат преобразования num1 равен 0. При преобразовании числовых типов обычно рекомендуется преобразовывать меньшие типы в большие, а не наоборот.
При использовании преобразования типов необходимо избегать неоднозначностей для некоторых типов. Пример:
*Point(p) // эквивалентно *(Point(p))
(*Point)(p) // преобразование p в тип *Point
<-chan int(c) // эквивалентно <-(chan int(c))
(<-chan int)(c) // преобразование c в тип <-chan int
(func())(x) // преобразование x в тип func()
(func() int)(x) // преобразование x в тип func() intУтверждение типа
Утверждение типа обычно используется для определения того, принадлежит ли переменная интерфейсного типа к определённому типу. Пример:
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}1Поскольку interface{} является пустым интерфейсным типом, который может представлять все типы, но тип int не может представлять тип interface{}, поэтому невозможно использовать преобразование типа. Утверждение типа позволяет определить, является ли базовый тип желаемым. Выражение утверждения типа имеет два возвращаемых значения: одно — значение после преобразования типа, другое — булево значение результата преобразования.
Определение типа
В Go оператор switch поддерживает специальный синтаксис, который позволяет обрабатывать различную логику в зависимости от различных case. Предварительным условием использования является то, что входной параметр должен быть интерфейсного типа. Пример:
var a interface{} = 2
switch a.(type) {
case int: fmt.Println("int")
case float64: fmt.Println("float")
case string: fmt.Println("string")
}intTIP
Используя операции, предоставляемые пакетом unsafe, можно обойти систему типов Go и выполнить операции преобразования типов, которые иначе не прошли бы компиляцию.
