Типы данных в 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) 
Output48 

Как видно на экране результатов, математический оператор - был использован для вычитания целого числа 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 

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

var sum = 564.0 + 365.24 fmt.Println(sum) 
Output929.24 

При работе с целыми числами и числами с плавающей точкой важно помнить, что 3 ≠ 3.0, поскольку 3 означает целое число, а 3.0 — число с плавающей точкой.

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

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

Сегодня большинство системных архитектур представляют собой 32-битные или 64-битные архитектуры. Например, вы можете разрабатывать приложения для современных ноутбуков с 64-битной операционной системой Windows. Однако, если вы разрабатывает приложения для таких устройств, как фитнес-браслеты, вам может потребоваться 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 сделает вашу программу более эффективной, поскольку для нее будет требоваться меньше памяти.

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

Переполнение и циклический переход

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

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

package main  import "fmt"  func main() {     var maxUint32 uint32 = 4294967295 // Max uint32 size     fmt.Println(maxUint32) } 

После компиляции и запуска получим следующий результат:

Output4294967295 

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

Output0 

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

package main  import "fmt"  func main() {     var maxUint32 uint32 = 4294967295 + 1     fmt.Println(maxUint32)  } 

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

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

Outputprog.go:6:36: constant 4294967296 overflows uint32 

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

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

Логические операторы

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

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

Многие математические операции дают ответы, соответствующие значениям «истина» или «ложь»:

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

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

myBool := 5 > 8 

Мы можем распечатать значение логического оператора посредством вызова функции fmt.Println():

fmt.Println(myBool) 

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

Outputfalse 

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

Строки

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

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

Необработанные литералы строк

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

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

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

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

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

OutputSay "hello" to Go!n 

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

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

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

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

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

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

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

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

UTF-8 — это схема кодировки, используемая для кодировки символов переменной ширины в 1-4 байтах. 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: %sn", i, string(c))     }     fmt.Println("length of 'Hello, 世界': ", len(a)) } 

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

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

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

В заключение мы распечатали полную переменную a с помощью встроенной функции len.

Мы уже упоминали, что rune является псевдонимом int32 и может состоять из 1-4 байт. Для определения символа требуется три байта, и индекс перемещается соответствующим образом при изменении диапазона в строке UTF-8. По этой причине печать i не выполняется последовательно.

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

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

Вы не всегда будете использовать строки UTF-8, но теперь вы понимаете, почему они относятся к типу rune, а не 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 

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

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

$s = "sammy";         // $s is automatically a string $s = 123;             // $s is automatically an integer 

В предыдущем блоке кода первая переменная $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] 

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

Карты

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

map[key]value{} 

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

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

Обратите внимание, что помимо фигурных скобок карта содержит двоеточия. Слова слева от двоеточий являются ключами. Ключи могут относится к любому *comparable *типу в Go. Сравнимые типы — это типы примитивов, в том числе строки, целые числа и т. д. Тип примитива определяется языком, а не составляется посредством сочетания других типов. Хотя допускается использование определяемых пользователем типов, во избежание ошибок программирования их лучше оставлять простыми. В словаре выше содержатся ключи: name, animal, color и location.

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

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

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

Если мы хотим изолировать цвет Sammy, мы можем использовать вызов sammy["color"]. Распечатаем результат:

fmt.Println(sammy["color"]) 
Outputblue 

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

Заключение

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

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