Понимание типов данных в Go

Введение

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

В этой статье мы рассмотрим важные типы данных, встроенные в Go. Это не исчерпывающее исследование типов данных, но поможет вам ознакомиться с доступными вам опциями в Go. Понимание некоторых базовых типов данных позволит вам писать более четкий код, который будет эффективно выполняться.

Предпосылки

Один из способов мыслить о типах данных — рассмотреть различные типы данных, которые мы используем в реальном мире. Примером данных в реальном мире являются числа: мы можем использовать целые числа (0, 1, 2, …), целые числа (…, -1, 0, 1, …) и иррациональные числа (π), например.

Обычно в математике мы можем комбинировать числа разных типов и получать какой-то ответ. Мы можем захотеть добавить 5 к π, например:

5 + π

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

5 + π = 5 + 3.14 = 8.14 

Но если мы начнем пытаться оценивать числа с использованием другого типа данных, например слов, то все станет менее понятным. Как бы мы решили следующее уравнение?

shark + 8

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

Целые числа

Как и в математике, целые числа в программировании — это целые числа, которые могут быть положительными, отрицательными или 0 (…, -1, 0, 1, …). В Go целое число известно как int. Как и в других языках программирования, не следует использовать запятые в числах из четырех и более цифр, поэтому когда вы пишете 1,000 в своей программе, пишите его как 1000.

Мы можем вывести целое число простым способом, например:

fmt.Println(-459)
Output
-459

Или мы можем объявить переменную, которая в данном случае является символом числа, которым мы пользуемся или манипулируем, вот так:

var absoluteZero int = -459
fmt.Println(absoluteZero)
Output
-459

Мы также можем производить математические операции с целыми числами в Go. В следующем блоке кода мы будем использовать оператор присваивания := для объявления и инициализации переменной sum:

sum := 116 - 68
fmt.Println(sum)
Output
48

Как видно из вывода, математический оператор - вычитал целое число 68 из 116, в результате чего получилось 48. Вы узнаете больше об объявлении переменных в разделе Объявление типов данных для переменных.

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

Числа с плавающей запятой

Число с плавающей запятой или float используется для представления действительных чисел, которые не могут быть выражены как целые. Действительные числа включают все рациональные и иррациональные числа, и поэтому числа с плавающей запятой могут содержать дробную часть, например 9.0 или -116.42. Для целей понимания float в программе на Go, это число, содержащее десятичную точку.

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

fmt.Println(-459.67)
Output
-459.67

Мы также можем объявить переменную, которая представляет собой float, вот так:

absoluteZero := -459.67
fmt.Println(absoluteZero)
Output
-459.67

Так же, как и с целыми числами, мы можем производить математические операции с float в Go:

var sum = 564.0 + 365.24
fmt.Println(sum)
Output
929.24

С целыми числами и числами с плавающей запятой важно помнить, что 3 ≠ 3.0, так как 3 относится к целому числу, а 3.0 к float.

Размеры числовых типов

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

Большинство системных архитектур сегодня являются либо 32-битными, либо 64-битными. Например, вы можете разрабатывать для современного ноутбука с Windows, на котором операционная система работает на 64-битной архитектуре. Однако, если вы разрабатываете для устройства, такого как фитнес-браслет, вы можете работать с 32-битной архитектурой. Если вы используете тип, независимый от архитектуры, например int32, независимо от архитектуры, для которой вы компилируете, тип будет иметь постоянный размер.

Второй тип — это зависящий от реализации тип. В этом типе размер в битах может варьироваться в зависимости от архитектуры, на которой собирается программа. Например, если мы используем тип int, когда Go компилируется для 32-битной архитектуры, размер типа данных будет 32 бита. Если программа компилируется для 64-битной архитектуры, переменная будет иметь размер 64 бита.

Помимо типов данных с разными размерами, такие типы, как целые числа, также бывают двух основных типов: со знаком и без знака. int8 — это целое число со знаком и может принимать значение от -128 до 127. uint8 — это целое число без знака и может принимать только положительные значения от 0 до 255.

Диапазоны основаны на размере в битах. Для двоичных данных 8 бит могут представлять в общей сложности 256 различных значений. Поскольку тип int должен поддерживать как положительные, так и отрицательные значения, 8-битное целое число (int8) будет иметь диапазон от -128 до 127, всего 256 уникальных возможных значений.

Go имеет следующие независимые от архитектуры целочисленные типы:

uint8       unsigned  8-bit integers (0 to 255)
uint16      unsigned 16-bit integers (0 to 65535)
uint32      unsigned 32-bit integers (0 to 4294967295)
uint64      unsigned 64-bit integers (0 to 18446744073709551615)
int8        signed  8-bit integers (-128 to 127)
int16       signed 16-bit integers (-32768 to 32767)
int32       signed 32-bit integers (-2147483648 to 2147483647)
int64       signed 64-bit integers (-9223372036854775808 to 9223372036854775807)

Числа с плавающей запятой и комплексные числа также имеют различные размеры:

float32     IEEE-754 32-bit floating-point numbers
float64     IEEE-754 64-bit floating-point numbers
complex64   complex numbers with float32 real and imaginary parts
complex128  complex numbers with float64 real and imaginary parts

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

byte        alias for uint8
rune        alias for int32

Целью псевдонима byte является явное указание на то, что ваша программа использует байты как общепринятую меру вычислений в элементах строки символов, в отличие от малых целых чисел, не связанных с измерением данных байтов. Несмотря на то, что byte и uint8 идентичны после компиляции программы, byte часто используется для представления символьных данных в числовой форме, тогда как uint8 предназначен для использования в качестве числа в вашей программе.

Псевдоним rune немного отличается. В то время как byte и uint8 являются точно одинаковыми данными, rune может быть одним байтом или четырьмя байтами, диапазон определяется int32. rune используется для представления символа Unicode, тогда как только ASCII символы могут быть представлены исключительно типом данных int32.

Кроме того, Go имеет следующие типы, зависящие от реализации:

uint     unsigned, either 32 or 64 bits
int      signed, either 32 or 64 bits
uintptr  unsigned integer large enough to store the uninterpreted bits of a pointer value 

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

Выбор числовых типов данных

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

Как обсуждалось ранее в этой статье, существуют типы, независимые от архитектуры, и типы, специфичные для конкретной реализации. Для целочисленных данных в Go обычно используются типы реализации, такие как int или uint, вместо int64 или uint64. Это, как правило, приводит к самой быстрой скорости обработки для вашей целевой архитектуры. Например, если вы используете int64 и компилируете для 32-битной архитектуры, обработка этих значений займет как минимум в два раза больше времени, так как потребуются дополнительные циклы процессора для перемещения данных через архитектуру. Если бы вы использовали int, программа определила бы его как 32-битный размер для 32-битной архитектуры и обработка была бы значительно быстрее.

Если вы знаете, что не превысите определенный диапазон размеров, то выбор типа, независимого от архитектуры, может как увеличить скорость, так и уменьшить использование памяти. Например, если вы знаете, что ваши данные не превысят значение 100 и будут только положительными числами, то выбор uint8 сделает вашу программу более эффективной, так как потребуется меньше памяти.

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

Переполнение vs. Заворачивание

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

В следующем примере мы устанавливаем maxUint32 в его максимальное значение:

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 // Максимальный размер uint32
	fmt.Println(maxUint32)
}

Он скомпилируется и запустится с следующим результатом:

Output
4294967295

Если мы добавим 1 к значению во время выполнения, оно завернется до 0:

Output
0

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

package main

import "fmt"

func main() {
	var maxUint32 uint32 = 4294967295 + 1
	fmt.Println(maxUint32)

}

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

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

Output
prog.go:6:36: constant 4294967296 overflows uint32

Понимание границ ваших данных поможет избежать потенциальных ошибок в вашей программе в будущем.

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

Булевы значения

Тип данных boolean может принимать одно из двух значений: либо true, либо false, и определяется как bool при объявлении его в качестве типа данных. Булевы значения используются для представления истинностных значений, связанных с логической ветвью математики, что влияет на алгоритмы в компьютерных науках.

Значения true и false всегда будут с маленькой буквы t и f соответственно, так как они являются предопределенными идентификаторами в Go.

Многие операции в математике дают нам ответы, которые оцениваются как истинные или ложные:

  • больше чем
    • 500 > 100 true
    • 1 > 5 false
  • меньше чем
    • 200 < 400 true
    • 4 < 2 false
  • равно
    • 5 = 5 true
    • 500 = 400 false

Как и с числами, мы можем хранить булево значение в переменной:

myBool := 5 > 8

Затем мы можем вывести булево значение, вызвав функцию fmt.Println():

fmt.Println(myBool)

Поскольку 5 не больше 8, мы получим следующий вывод:

Output
false

По мере написания большего количества программ на Go, вы будете лучше понимать, как работают булевы значения и как различные функции и операции, возвращающие либо true, либо false, могут изменять ход программы.

Строки

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

Если вы используете обратные апострофы, вы создаете сырой строковый литерал. Если вы используете двойные кавычки, вы создаете интерпретируемый строковый литерал.

Сырые строковые литералы

Сырые строковые литералы — это последовательности символов между обратными апострофами, которые часто называют обратными кавычками. Внутри кавычек любой символ будет отображаться точно так же, как он отображается между обратными апострофами, за исключением самого символа обратного апострофа.

a := `Say "hello" to Go!`
fmt.Println(a)
Output
Say "hello" to Go!

Обычно обратные слеши используются для представления специальных символов в строках. Например, в интерпретируемой строке \n представляет новую строку. Однако, обратные слеши не имеют специального значения внутри строковых литералов в “сыром” формате:

a := `Say "hello" to Go!\n`
fmt.Println(a)

Поскольку обратный слеш не имеет специального значения в строковом литерале, он фактически выведет значение \n вместо создания новой строки:

Output
Say "hello" to Go!\n

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

a := `This string is on 
multiple lines
within a single back 
quote on either side.`
fmt.Println(a)
Output
This string is on multiple lines within a single back quote on either side.

В предыдущих блоках кода новые строки переносились буквально от ввода к выводу.

Интерпретируемые строковые литералы

Интерпретируемые строковые литералы — это последовательности символов между двойными кавычками, как в "bar". Внутри кавычек могут появляться любые символы, кроме новой строки и неэкранированных двойных кавычек. Чтобы показать двойные кавычки в интерпретируемой строке, можно использовать обратный слеш как escape-символ, например:

a := "Say \"hello\" to Go!"
fmt.Println(a)
Output
Say "hello" to Go!

Вы почти всегда будете использовать интерпретируемые строковые литералы, так как они позволяют использовать escape-символы внутри них. Для получения дополнительной информации о работе со строками, ознакомьтесь с Введение в работу со строками в Go.

Строки с символами UTF-8

UTF-8 — это схема кодирования, используемая для кодирования символов переменной ширины в один-четыре байта. Go поддерживает символы UTF-8 из коробки, без какой-либо специальной настройки, библиотек или пакетов. Римские символы, такие как буква A, могут быть представлены значением ASCII, таким как число 65. Однако для специальных символов, таких как международный символ , потребуется UTF-8. Go использует тип-псевдоним rune для данных UTF-8.

a := "Hello, 世界"

Вы можете использовать ключевое слово range в цикле for для индексации по любой строке в Go, даже по строке UTF-8. Циклы for и range будут рассмотрены более подробно позже в серии; пока важно знать, что мы можем использовать это для подсчета байтов в данной строке:

package main

import "fmt"

func main() {
	a := "Hello, 世界"
	for i, c := range a {
		fmt.Printf("%d: %s\n", i, string(c))
	}
	fmt.Println("length of 'Hello, 世界': ", len(a))
}

В приведенном выше блоке кода мы объявили переменную a и присвоили ей значение Hello, 世界. Присвоенный текст содержит символы UTF-8.

Затем мы использовали стандартный цикл for и ключевое слово range. В Go ключевое слово range будет индексировать строку, возвращая по одному символу за раз, а также байтовый индекс, по которому находится символ в строке.

Используя функцию fmt.Printf, мы предоставили строку формата %d: %s\n. %d является глаголом печати для цифры (в данном случае целого числа), а %s — глаголом печати для строки. Затем мы предоставили значения i, которое является текущим индексом цикла for, и c, которое является текущим символом в цикле for.

Наконец, мы вывели общую длину переменной a, используя встроенную функцию len.

Ранее мы упоминали, что руна является псевдонимом для int32 и может состоять из одного до четырех байтов. Символ занимает три байта для определения, и индекс соответственно перемещается при обходе строки UTF-8. Вот почему i не является последовательным, когда он выводится.

Output
0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界': 13

Как видите, длина больше, чем количество раз, которое потребовалось для обхода строки.

Вы не всегда будете работать со строками UTF-8, но когда будете, теперь вы понимаете, почему они являются рунами, а не одним int32.

Объявление типов данных для переменных

Теперь, когда вы знаете о различных примитивных типах данных, мы рассмотрим, как присваивать эти типы переменным в Go.

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

В следующем примере мы объявим переменную с именем pi типа float64.

Ключевое слово var является первым объявленным элементом:

var pi float64

За ним следует имя нашей переменной, pi:

var pi float64

И, наконец, тип данных float64:

var pi float64

Мы также можем опционально указать начальное значение, например 3.14:

var pi float64 = 3.14

Go — это язык со статической типизацией. Статическая типизация означает, что каждое выражение в программе проверяется во время компиляции. Это также означает, что тип данных привязан к переменной, тогда как в языках с динамической типизацией тип данных привязан к значению.

Например, в Go тип объявляется при объявлении переменной:

var pi float64 = 3.14
var week int = 7

Каждая из этих переменных может быть другого типа данных, если вы объявите их по-другому.

Это отличается от языка, такого как PHP, где тип данных связан со значением:

$s = "sammy";         // $s автоматически является строкой
$s = 123;             // $s автоматически является целым числом

В предыдущем блоке кода первый $s является строкой, потому что ему присвоено значение "sammy", а второй — целым числом, потому что он имеет значение 123.

Далее, давайте рассмотрим более сложные типы данных, такие как массивы.

Массивы

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

Массивы определяются путем объявления размера массива, затем типа данных с значениями, определенными в фигурных скобках { }.

Массив строк выглядит следующим образом:

[3]string{"blue coral", "staghorn coral", "pillar coral"}

Мы можем сохранить массив в переменной и вывести его:

coral := [3]string{"blue coral", "staghorn coral", "pillar coral"}
fmt.Println(coral)
Output
[blue coral staghorn coral pillar coral]

Как упоминалось ранее, срезы похожи на массивы, но гораздо более гибкие. Давайте рассмотрим это изменяемый тип данных.

Срезы

срез представляет собой упорядоченную последовательность элементов, длина которой может изменяться. Срезы могут динамически увеличивать свой размер. Когда вы добавляете новые элементы в срез, если у среза недостаточно памяти для хранения новых элементов, он будет запрашивать больше памяти у системы по мере необходимости. Поскольку срез может быть расширен для добавления новых элементов по мере необходимости, они используются чаще, чем массивы.

Срезы определяются путем объявления типа данных, перед которым стоят открывающая и закрывающая квадратные скобки [], и значений в фигурных скобках { }.

Срез целых чисел выглядит так:

[]int{-3, -2, -1, 0, 1, 2, 3}

Срез чисел с плавающей запятой выглядит так:

[]float64{3.14, 9.23, 111.11, 312.12, 1.05}

Срез строк выглядит так:

[]string{"shark", "cuttlefish", "squid", "mantis shrimp"}

Определим наш срез строк как seaCreatures:

seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"}

Мы можем вывести их, вызвав переменную:

fmt.Println(seaCreatures)

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

Output
[shark cuttlefish squid mantis shrimp]

Мы можем использовать ключевое слово append для добавления элемента в наш срез. Следующая команда добавит строковое значение seahorse в срез:

seaCreatures = append(seaCreatures, "seahorse")

Вы можете убедиться, что он был добавлен, выведя его:

fmt.Println(seaCreatures)
Output
[shark cuttlefish squid mantis shrimp seahorse]

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

Карты

map — это встроенный в Go тип хеш-таблицы или словаря. Карты используют ключи и значения в качестве пары для хранения данных. Это полезно в программировании для быстрого поиска значений по индексу или, в данном случае, по ключу. Например, вы можете захотеть сохранить карту пользователей, индексированную по их ID. Ключом будет ID пользователя, а объектом пользователя — значение. Карта создается с использованием ключевого слова map, за которым следует тип данных ключа в квадратных скобках [ ], затем тип данных значения и пары ключ-значение в фигурных скобках.

map[key]value{}

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

map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}

Вы заметите, что помимо фигурных скобок, в карте также присутствуют двоеточия. Слова слева от двоеточий являются ключами. Ключи могут быть любым сравнимым типом в Go. Сравнимые типы — это примитивные типы, такие как строки, целые числа и т.д. Примитивный тип определяется языком и не создается путем объединения других типов. Хотя они могут быть пользовательскими типами, считается лучшей практикой сохранять их простыми, чтобы избежать ошибок программирования. Ключами в словаре выше являются: имя, животное, цвет и местоположение.

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

Давайте сохраним карту в переменной и выведем ее:

sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
fmt.Println(sammy)
Output
map[animal:shark color:blue location:ocean name:Sammy]

Если мы хотим изолировать цвет Сэмми, мы можем сделать это, вызвав sammy["цвет"]. Давайте выведем это:

fmt.Println(sammy["color"])
Output
blue

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

Заключение

На данном этапе вы должны лучше понимать некоторые из основных типов данных, доступных для использования в Go. Каждый из этих типов данных станет важным по мере разработки программных проектов на языке Go.

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

Source:
https://www.digitalocean.com/community/tutorials/understanding-data-types-in-go