У Go очень интересная система типов. Он отказывается от классов и наследования в пользу интерфейсов и композиции, но, с другой стороны, не имеет шаблонов или шаблонов. То, как он обрабатывает коллекции, также уникально.
В этом руководстве вы узнаете о тонкостях системы типов Go и о том, как эффективно использовать ее для написания понятного и идиоматического кода Go.
Большая картина системы типов Go
Система типов Go поддерживает процедурные, объектно-ориентированные и функциональные парадигмы. У него очень ограниченная поддержка универсального программирования. Хотя Go является абсолютно статическим языком, он обеспечивает достаточную гибкость для динамических методов через интерфейсы, функции первого класса и рефлексию. В системе типов Go отсутствуют возможности, которые характерны для большинства современных языков:
- Тип исключения отсутствует, поскольку обработка ошибок Go основана на кодах возврата и интерфейсе ошибок.
- Перегрузка оператора отсутствует.
- Перегрузка функции отсутствует (одно и то же имя функции с разными параметрами).
- Не существует дополнительных или стандартных параметров функции.
Все эти упущения созданы для того, чтобы сделать Go максимально простым.
Введите псевдонимы
Вы можете создавать псевдонимы в Go и создавать различные типы. Вы не можете присвоить значение базового типа псевдониму без преобразования. Например, присваивание var b int = a
в следующей программе вызывает ошибку компиляции, поскольку тип Age
является псевдонимом int, но не является int:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
package main
type Age int
func main() {
var a Age = 5
var b int = a
}
Output:
tmp/sandbox547268737/main.go:8: cannot use a (type Age) as
type int in assignment
|
Вы можете группировать объявления типов или использовать одно объявление на строку:
1
2
3
4
5
6
7
8
|
type IntIntMap map[int]int
StringSlice []string
type (
Size uint64
Text string
CoolFunc func(a int, b bool)(int, error)
)
|
Основные типы
Все обычные подозреваемые здесь: bool, string, целые и целые числа без знака с явными размерами битов, числа с плавающей запятой (32-битные и 64-битные) и комплексные числа (64-битные и 128-битные).
1
2
3
4
5
6
7
8
|
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32, represents a Unicode code point
float32 float64
complex64 complex128
|
Струны
Строки в Go имеют кодировку UTF8 и поэтому могут представлять любой символ Unicode. Пакет strings обеспечивает множество операций со строками. Вот пример взятия массива слов, преобразования их в правильный регистр и соединения их в предложение.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
«fmt»
«strings»
)
func main() {
words := []string{«i», «LikE», «the ColORS:», «RED,»,
«bLuE,», «AnD», «GrEEn»}
properCase := []string{}
for i, w := range words {
if i == 0 {
properCase = append(properCase, strings.Title(w))
} else {
properCase = append(properCase, strings.ToLower(w))
}
}
sentence := strings.Join(properCase, » «) + «.»
fmt.Println(sentence)
}
|
указатели
Go имеет указатели. Нулевой указатель (см. Нулевые значения позже) равен нулю. Вы можете получить указатель на значение с помощью оператора &
и вернуться назад с помощью оператора *
. Вы можете иметь указатели на указатели тоже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
«fmt»
)
type S struct {
a float64
b string
}
func main() {
x := 5
px := &x
*px = 6
fmt.Println(x)
ppx := &px
**ppx = 7
fmt.Println(x)
}
|
Объектно-ориентированного программирования
Go поддерживает объектно-ориентированное программирование через интерфейсы и структуры. Нет классов и иерархии классов, хотя вы можете встраивать анонимные структуры в структуры, которые обеспечивают своего рода единственное наследование.
Для детального изучения объектно-ориентированного программирования в Go, посмотрите Go Go: Объектно-ориентированное программирование в Golang .
Интерфейсы
Интерфейсы являются краеугольным камнем системы типов Go. Интерфейс — это просто набор сигнатур методов. Каждый тип, который реализует все методы, совместим с интерфейсом. Вот быстрый пример. Интерфейс Shape
определяет два метода: GetPerimeter()
и GetArea()
. Объект Square
реализует интерфейс.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
type Shape interface {
GetPerimeter() uint
GetArea() uint
}
type Square struct {
side uint
}
func (s *Square) GetPerimeter() uint {
return s.side * 4
}
func (s *Square) GetArea() uint {
return s.side * s.side
}
|
Пустой интерфейсный interface{}
совместим с любым типом, потому что нет никаких методов, которые требуются. Пустой интерфейс может указывать на любой объект (аналог Java-объекта или пустого указателя C / C ++) и часто используется для динамической типизации. Интерфейсы всегда указатели и всегда указывают на конкретный объект.
Для всей статьи об интерфейсах Go, посмотрите: Как определить и реализовать интерфейс Go .
Структуры
Структуры — это пользовательские типы Go. Структура содержит именованные поля, которые могут быть базовыми типами, типами указателей или другими типами структур. Вы также можете анонимно встраивать структуры в другие структуры как форму наследования реализации.
В следующем примере структуры S1 и S2 встроены в структуру S3, которая также имеет свое собственное поле int
и указатель на свой собственный тип:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
package main
import (
«fmt»
)
type S1 struct {
f1 int
}
type S2 struct {
f2 int
}
type S3 struct {
S1
S2
f3 int
f4 *S3
}
func main() {
s := &S3{S1{5}, S2{6}, 7, nil}
fmt.Println(s)
}
Output:
&{{5} {6} 7 <nil>}
|
Тип Утверждения
Утверждения типа позволяют преобразовать интерфейс в его конкретный тип. Если вы уже знаете базовый тип, вы можете просто подтвердить его. Если вы не уверены, вы можете попробовать несколько утверждений типа, пока не найдете нужный тип.
В следующем примере есть список вещей, который содержит строки и не строковые значения, представленные в виде фрагмента пустых интерфейсов. Код перебирает все вещи, пытаясь преобразовать каждый элемент в строку и сохранить все строки в отдельном фрагменте, который он в конечном итоге печатает.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import «fmt»
func main() {
things := []interface{}{«hi», 5, 3.8, «there», nil, «!»}
strings := []string{}
for _, t := range things {
s, ok := t.(string)
if ok {
strings = append(strings, s)
}
}
fmt.Println(strings)
}
Output:
[hi there !]
|
отражение
Пакет Go reflect
позволяет напрямую проверять тип интерфейса без утверждений типа. Вы также можете извлечь значение интерфейса и преобразовать его в интерфейс, если хотите (не так полезно).
Вот пример, аналогичный предыдущему, но вместо печати строк он просто считает их, поэтому нет необходимости преобразовывать interface{}
в string
. Ключ вызывает reflect.Type()
для получения объекта типа, у которого есть метод Kind()
который позволяет нам определить, имеем ли мы дело со строкой или нет.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
«fmt»
«reflect»
)
func main() {
things := []interface{}{«hi», 5, 3.8, «there», nil, «!»}
stringCount := 0
for _, t := range things {
tt := reflect.TypeOf(t)
if tt != nil && tt.Kind() == reflect.String {
stringCount++
}
}
fmt.Println(«String count:», stringCount)
}
|
функции
Функции первоклассных граждан в Go. Это означает, что вы можете назначать функции переменным, передавать функции в качестве аргументов другим функциям или возвращать их в качестве результатов. Это позволяет вам использовать функциональный стиль программирования с Go.
В следующем примере демонстрируется пара функций, GetUnaryOp()
и GetBinaryOp()
, которые возвращают анонимные функции, выбранные случайным образом. Основная программа решает, нужна ли ей унарная операция или двоичная операция, основываясь на количестве аргументов. Он сохраняет выбранную функцию в локальной переменной с именем «op», а затем вызывает ее с правильным количеством аргументов.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
package main
import (
«fmt»
«math/rand»
)
type UnaryOp func(a int) int
type BinaryOp func(a, b int) int
func GetBinaryOp() BinaryOp {
if rand.Intn(2) == 0 {
return func(a, b int) int { return a + b }
} else {
return func(a, b int) int { return a — b }
}
}
func GetUnaryOp() UnaryOp {
if rand.Intn(2) == 0 {
return func(a int) int { return -a }
} else {
return func(a int) int { return a * a }
}
}
func main() {
arguments := [][]int{{4,5},{6},{9},{7,18},{33}}
var result int
for _, a := range arguments {
if len(a) == 1 {
op := GetUnaryOp()
result = op(a[0])
} else {
op := GetBinaryOp()
result = op(a[0], a[1])
}
fmt.Println(result)
}
}
|
каналы
Каналы — это необычный тип данных. Вы можете думать о них как об очередях сообщений, используемых для передачи сообщений между программами. Каналы строго типизированы. Они синхронизированы и имеют специальную поддержку синтаксиса для отправки и получения сообщений. Каждый канал может быть только для приема, только для отправки или двунаправленным.
Каналы также могут быть дополнительно буферизированы. Вы можете перебирать сообщения в канале, используя диапазон, и подпрограммы go могут блокировать несколько каналов одновременно, используя универсальную операцию выбора.
Вот типичный пример, где сумма квадратов списка целых чисел вычисляется параллельно двумя процедурами go, каждая из которых отвечает за половину списка. Основная функция ожидает результатов от обеих подпрограмм go и затем суммирует частичные суммы для итога. Обратите внимание, как канал c
создается с помощью встроенной функции make()
и как код читает и записывает в канал через специальный оператор <-
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import «fmt»
func sum_of_squares(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v * v
}
c <- sum // send sum to c
}
func main() {
s := []int{11, 32, 81, -9, -14}
c := make(chan int)
go sum_of_squares(s[:len(s)/2], c)
go sum_of_squares(s[len(s)/2:], c)
sum1, sum2 := <-c, <-c // receive from c
total := sum1 + sum2
fmt.Println(sum1, sum2, total)
}
|
Это просто чистка поверхности. Для подробного просмотра каналов, проверьте:
Коллекции
Go имеет несколько встроенных общих коллекций, которые могут хранить любой тип. Эти коллекции особенные, и вы не можете определить свои собственные общие коллекции. Коллекции представляют собой массивы, фрагменты и карты. Каналы также являются общими и могут рассматриваться как коллекции, но у них есть довольно уникальные свойства, поэтому я предпочитаю обсуждать их отдельно.
Массивы
Массивы представляют собой наборы элементов фиксированного размера одного типа. Вот несколько массивов:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
package main
import «fmt»
func main() {
a1 := [3]int{1, 2, 3}
var a2 [3]int
a2 = a1
fmt.Println(a1)
fmt.Println(a2)
a1[1] = 7
fmt.Println(a1)
fmt.Println(a2)
a3 := [2]interface{}{3, «hello»}
fmt.Println(a3)
}
|
Размер массива является частью его типа. Вы можете копировать массивы одного типа и размера. Копия по стоимости. Если вы хотите хранить элементы другого типа, вы можете использовать escape-массив массива пустых интерфейсов.
Ломтики
Массивы довольно ограничены из-за их фиксированного размера. Ломтики гораздо интереснее. Вы можете думать о срезах как о динамических массивах. Под крышками срезы используют массив для хранения своих элементов. Вы можете проверить длину среза, добавить элементы и другие срезы, и самое интересное, что вы можете извлечь срезы, похожие на нарезку в Python:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import «fmt»
func main() {
s1 := []int{1, 2, 3}
var s2 []int
s2 = s1
fmt.Println(s1)
fmt.Println(s2)
// Modify s1
s1[1] = 7
// Both s1 and s2 point to the same underlying array
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(len(s1))
// Slice s1
s3 := s1[1:len(s1)]
fmt.Println(s3)
}
|
Когда вы копируете фрагменты, вы просто копируете ссылку на тот же базовый массив. При срезе подрежим все еще указывает на тот же массив. Но когда вы добавляете, вы получаете срез, который указывает на новый массив.
Вы можете перебирать массивы или фрагменты, используя обычный цикл с индексами или используя диапазоны. Вы также можете создавать срезы с заданной емкостью, которые будут инициализированы нулевым значением их типа данных, используя функцию make()
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
package main
import «fmt»
func main() {
// Create a slice of 5 booleans initialized to false
s1 := make([]bool, 5)
fmt.Println(s1)
s1[3] = true
s1[4] = true
fmt.Println(«Iterate using standard for loop with index»)
for i := 0;
fmt.Println(i, s1[i])
}
fmt.Println(«Iterate using range»)
for i, x := range(s1) {
fmt.Println(i, x)
}
}
Output:
[false false false false false]
Iterate using standard for loop with index
0 false
1 false
2 false
3 true
4 true
Iterate using range
0 false
1 false
2 false
3 true
4 true
|
Карты
Карты — это коллекции пар ключ-значение. Вы можете назначить им литералы карты или другие карты. Вы также можете создавать пустые карты, используя встроенную функцию make
. Вы получаете доступ к элементам, используя квадратные скобки. Карты поддерживают итерацию с использованием range
, и вы можете проверить, существует ли ключ, попытавшись получить к нему доступ и проверив второе необязательное логическое возвращаемое значение.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
package main
import (
«fmt»
)
func main() {
// Create map using a map literal
m := map[int]string{1: «one», 2: «two», 3:»three»}
// Assign to item by key
m[5] = «five»
// Access item by key
fmt.Println(m[2])
v, ok := m[4]
if ok {
fmt.Println(v)
} else {
fmt.Println(«Missing key: 4»)
}
for k, v := range m {
fmt.Println(k, «:», v)
}
}
Output:
two
Missing key: 4
5 : five
1 : one
2 : two
3 : three
|
Обратите внимание, что итерация не в порядке создания или вставки.
Нулевые значения
В Go нет неинициализированных типов. Каждый тип имеет предопределенное нулевое значение. Если переменная типа объявлена без присвоения ей значения, то она содержит нулевое значение. Это важная функция безопасности типа.
Для любого типа T
, *new(T)
вернет одно нулевое значение T
Для логических типов нулевое значение равно «ложь». Для числовых типов нулевое значение равно … нулю. Для срезов, карт и указателей это ноль. Для структур это структура, в которой все поля инициализируются нулевым значением.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
«fmt»
)
type S struct {
a float64
b string
}
func main() {
fmt.Println(*new(bool))
fmt.Println(*new(int))
fmt.Println(*new([]string))
fmt.Println(*new(map[int]string))
x := *new([]string)
if x == nil {
fmt.Println(«Uninitialized slices are nil»)
}
y := *new(map[int]string)
if y == nil {
fmt.Println(«Uninitialized maps are nil too»)
}
fmt.Println(*new(S))
}
|
А как насчет шаблонов или обобщений?
Иди не имеет. Это, наверное, самая распространенная жалоба на систему типов Go. Дизайнеры Go открыты для этой идеи, но пока не знают, как ее реализовать, не нарушая другие принципы дизайна, лежащие в основе языка. Что вы можете сделать, если вам очень нужны некоторые общие типы данных? Вот несколько предложений:
- Если у вас есть только несколько экземпляров, подумайте о создании конкретных объектов.
- Используйте пустой интерфейс (вам нужно будет ввести assert обратно к вашему конкретному типу в какой-то момент).
- Используйте генерацию кода.
Вывод
У Go есть интересная система типов. Дизайнеры Go приняли четкие решения, чтобы остаться на простой стороне спектра. Если вы серьезно относитесь к программированию на Go, вам следует потратить время и узнать о его системе типов и ее особенностях. Это будет стоить вашего времени.