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

Вступление

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

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

Фон

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

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

5 + π

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

5 + π = 5 + 3.14 = 8.14

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

shark + 8

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

Целые

Как и в математике, в компьютерном программированииintegers - это целые числа, которые могут быть положительными, отрицательными или 0 (…, -1, 0, 1,…). В Go целое число известно какint. Как и в других языках программирования, вы не должны использовать запятые в числах из четырех и более цифр, поэтому, когда вы пишете 1000 в своей программе, запишите это как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. Вы узнаете больше об объявлении переменных в разделеDeclaring Data Types for Variables.

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

Числа с плавающей точкой

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

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

fmt.Println(-459.67)
Output-459.67

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

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 имеет два типа числовых данных, которые отличаются статическим или динамическим характером их размеров. Первый тип - это типarchitecture-independent, что означает, что размер данных в битах не меняется, независимо от машины, на которой выполняется код.

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

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

Помимо типов данных, имеющих разные размеры, такие типы, как целые числа, также бывают двух основных типов:signed иunsigned. 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 - это одни и те же данные, arune может быть одним или четырьмя байтами, диапазон определяетсяint32. rune используется для представления символа Юникода, тогда как только символы 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 сделает вашу программу более эффективной, так как она потребует меньше памяти.

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

Переполнение против Wraparound

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

В следующем примере мы устанавливаем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)

}

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

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

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

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

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

Булевы

Тип данныхboolean может быть одним из двух значений,true илиfalse, и определяется как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, вы лучше узнаете, как работают логические значения и как различные функции и операции, оцениваемые какtrue илиfalse, могут изменить ход программы.

Струны

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

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

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

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

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

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

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

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

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". В кавычках может появляться любой символ, кроме новой строки и неэкранированных двойных кавычек. Чтобы показать двойные кавычки в интерпретируемой строке, вы можете использовать обратную косую черту в качестве escape-символа, например так:

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

Вы почти всегда будете использовать интерпретированные строковые литералы, потому что они допускают использование escape-символов внутри них. Дополнительные сведения о работе со строками см. ВAn Introduction to Working with Strings in 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 . %d - это команда печати для цифры (в данном случае целого числа), а%s - это команда печати для строки. Затем мы предоставили значенияi, который является текущим индексом циклаfor, иc, который является текущим символом в циклеfor.

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

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

Output0: 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 - это языкstatically typed. Статически типизированный означает, что каждый оператор в программе проверяется во время компиляции. Это также означает, что тип данных связан с переменной, тогда как в динамически связанных языках тип данных связан со значением.

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

var pi float64 = 3.14
var week int = 7

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

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

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

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

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

Массивы

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

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

Массив строк выглядит так:

[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]

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

Ломтики

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

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

Ломтик целых чисел выглядит так:

[]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]

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

Maps

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

map[key]value{}

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

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

Вы заметите, что в дополнение к фигурным скобкам на карте также есть двоеточия. Слова слева от двоеточия являются ключами. Ключи могут быть любого типаcomparable в Go. Сопоставимые типы - это примитивные типы, такие какstrings,ints и т. Д. Примитивный тип определяется языком, а не создается из объединения каких-либо других типов. Несмотря на то, что они могут быть пользовательскими типами, рекомендуется сохранять их простыми, чтобы избежать ошибок программирования. Ключи в словаре выше: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["color"]. Давайте распечатать это:

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

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

Заключение

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

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