Статьи

Глубокое погружение в систему типов 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)
}

Это просто чистка поверхности. Для подробного просмотра каналов, проверьте:

  • Идти
    Пойдем: параллелизм Голанга, часть 2
    Гиги Сайфан

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, вам следует потратить время и узнать о его системе типов и ее особенностях. Это будет стоить вашего времени.