В этом разделе мы научим Вас тому, как определять константы, переменные, относящиеся к элементарным типам данных, а также некоторым приемам программирования на Go.
В Go существует множество способов определить переменную.
Основной способ определить переменную в Go - с помощью ключевого слова 'var'. Заметьте, что в Go тип переменной ставится после
ее имени:
// Определяем переменную “variableName” и тип "type"
var variableName type
Определение множества переменных:
// Определяем три переменных типа "type"
var vname1, vname2, vname3 type
Определение переменной с присваиванием ей значения:
// Определяем переменную “variableName” типа "type" и задаем ей значение "value"
var variableName type = value
Определение множества переменных с присваиванием им значений:
/*
Определям три переменные типа "type" и инициализируем их значения.
vname1 равно v1, vname2 равно v2, vname3 равно v3
*/
var vname1, vname2, vname3 type = v1, v2, v3
Вам не кажется слишком скучным определять переменные способом, указанным выше? Не волнуйтесь, команда разработчиков Go также посчитала это проблемой. Поэтому если Вы хотите определить переменные с начальными значениями, можно просто опустить указание типа переменных, и код будет выглядеть следующим образом:
/*
Определям три переменные типа "type" и инициализируем их значения.
vname1 равно v1, vname2 равно v2, vname3 равно v3
*/
var vname1, vname2, vname3 = v1, v2, v3
Да, я понимаю, что этого недостаточно. Исправить это мы можем так:
/*
Определяем три переменные, не используя ключевые слова "type" и "var", и задаем им начальные значения.
vname1 равно v1, vname2 равно v2, vname3 равно v3
*/
vname1, vname2, vname3 := v1, v2, v3
Так уже гораздо лучше. Используйте :=
для замены var
и type
, это называется коротким объявлением. Но есть одно ограничение: такую форму определения можно использовать только внутри функций. Если Вы попытаетесь использовать ее вне тела функции, Вы получите ошибку компиляции. Поэтому можно использовать var
для определения глобальных переменных и короткие объявления в var()
.
_
(blank) - это специальное имя переменной. Любое значение, присвоенное такой переменной, будет проигнорировано. Например, мы присваиваем 35
переменной b
и пропускаем 34
( Этот пример просто призван показать, как это работает. Здесь не видно, в чем его польза, но мы будем часто использовать эту возможность Go в работе со значениями, возвращаемыми функциями. ):
_, b := 34, 35
Если Вы определили переменную и не использовали ее нигде в своей программе, компилятор покажет Вам ошибку компиляции. Попробуйте скомпилировать следующий код и посмотрите, что будет:
package main
func main() {
var i int
}
Так называемые константы - это значения, которые были определены во время компиляции, и их нельзя изменить во время работы программы. В Go в качестве типов констант можно использовать число, булев тип и строку.
Константы определяются следующим образом:
const constantName = value
// Если нужно, можно задать тип константы
const Pi float32 = 3.1415926
Еще примеры:
const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"
в Go для определения переменной булева типа используется bool
, значение ее может быть только true
или false
, и false
- это значение по умолчанию. ( Нельзя конвертировать булев тип в числовой и наоборот! )
// пример кода
var isActive bool // глобальная переменная
var enabled, disabled = true, false // опускаем тип переменной
func test() {
var available bool // локальная переменная
valid := false // краткое объявление переменной
available = true // присваивание значения переменной
}
Целочисленные типы включают в себя как целочисленные типы со знаком, так и без знака. В Go есть и int
, и uint
, у них одинаковая длина, но в каждом конкретном случае она зависит от операционной системы. В 32-битных системах используются 32-битные типы, в 64-битных - 64-битные. В Go также есть типы, у которых особая длина. К ним относятся rune
, int8
, int16
, int32
, int64
, byte
, uint8
, uint16
, uint32
, uint64
. Заметьте, что rune
- это алиас int32
, а byte
- это алиас uint8
.
Есть одна важная вещь, которую надо знать - Вы не можете комбинировать разные типы в одном выражении, такая операция повлечет ошибку компиляции:
var a int8
var b int32
c := a + b
Хотя int32 длиннее int8 и является тем же типом, что и int, нельзя использовать их в одним выражении. ( 'c' здесь будет определена как переменная типа int
)
К типам с плавающей точкой относятся float32
и float64
; типа, называемого float
, в Go нет. float64
используется по умолчанию при коротком объявлении.
Это все? Нет! Go также поддерживает и комплексные числа. complex128
(с 64-битной вещественной и 64-битной мнимыми частями) является комплексным числом по умолчанию, а если Вам нужны числа поменьше, есть complex64
(с 32-битной вещественной и 32-битной нмимыми частями). Числа представлены в форме RE+IMi
, где RE
- вещественная часть, а IM
- мнимая, последнее i
- мнимая единица. Вот пример комплексного числа:
var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Значение: %v", c)
Мы уже говорили о том, что Go использует кодировку UTF-8. Строки представлены двойными кавычками ""
или обратными кавычками ``
.
// Пример кода
var frenchHello string // основная форма определения строки
var emptyString string = "" // определяем строковую переменную с пустым значением
func test() {
no, yes, maybe := "no", "yes", "maybe" // короткое объявление
japaneseHello := "Ohaiou"
frenchHello = "Bonjour" // основная форма присваивания переменной значения
}
Менять отдельные символы в строках по индексу невозможно. Например, при компиляции следующего кода вы получите ошибку:
var s string = "hello"
s[0] = 'c'
Что если нам действительно хочется изменить лишь один символ в строке? Попробуем следующее:
s := "hello"
c := []byte(s) // конвертируем строку в тип []byte
c[0] = 'c'
s2 := string(c) // конвертируем []byte обратно в строку
fmt.Printf("%s\n", s2)
Для того, чтобы объединить две строки, используйте оператор +
:
s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)
Также:
s := "hello"
s = "c" + s[1:] // нельзя менять строку по индексу, но получать значения по индексу можно
fmt.Printf("%s\n", s)
Что, если мы захотим определить строковую переменную, значение которой располагается на разных строках?
m := `hello
world`
`
все символы в строке воспринимает буквально, как часть значения переменной.
В Go есть один тип error
, предназначенный для работы с сообщениями об ошибках. Также есть пакет errors
для обработки ошибок.
err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
fmt.Print(err)
}
Следующий рисунок приведен из статьи про структуру данных в Go в блоге Russ Cox. Для хранения данных Go использует блоки памяти.
Рисунок 2.1 Структура, лежащая в основе данных в Go.
Если Вы хотите определить сразу несколько констант, переменных или импортировать несколько пакетов, Вы можете использовать групповое определение.
Основная форма:
import "fmt"
import "os"
const i = 100
const pi = 3.1415
const prefix = "Go_"
var i int
var pi float32
var prefix string
Групповое определение:
import(
"fmt"
"os"
)
const(
i = 100
pi = 3.1415
prefix = "Go_"
)
var(
i int
pi float32
prefix string
)
Если не указать, что значение константы равно iota
, то значение первой константы в группе const()
будет равно 0
. Если же константе, перед которой есть другая константа, явно не присвоено никакого значения, оно будет равно значению идущей перед ней константы. Если значение константы указано как iota
, значения последующих после нее констант в группе, которым явно не присвоено никаких значений, также будут равны iota
(И так до тех пор, пока не встретится константа с явно указанным значением, после этого значения всех идущих после нее констант с явно неуказанным значением будут равны ее значению - прим. переводчика на русский).
В Go есть ключевое слово iota
, оно служит для того, чтобы обеспечить последовательное перечисление (enum
), оно начинается с 0
и с каждым шагом увеличивается на 1
.
const(
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // если значение константы не указано, ей присваивается значение идущей перед ней константы, следовательно здесь также получается w = iota. Поэтому w == 3, а в случаях y и x также можно было бы опустить "= iota".
)
const v = iota // так как iota встречает ключевое слово `const`, происходит сброс на 0, поэтому v = 0.
const (
e, f, g = iota, iota, iota // e=0,f=0,g=0, значения iota одинаковы, так как находятся на одной строке.
)
Причина краткости кода, написанного на Go - это то, что этому языку присущи некоторые моменты поведения по умолчанию:
- Все переменные, имя которых начинается с большой буквы, являются публичными; те, имя которых начинается с маленькой буквы - приватными.
- То же относится к функциям и константам, в Go нет ключевых слов
public
илиprivate
.
Массив в Go определяется так:
var arr [n]тип
В [n]тип
, n
- длина массива, type
- тип его элементов. Так же, как и в других языках, квадратные скобки []
используются для того, чтобы получить или присвоить значения элементам массива.
var arr [10]int // массив типа [10]int
arr[0] = 42 // первый элемент массива имеет индекс 0
arr[1] = 13 // присваивание значения элементу массива
fmt.Printf("Первый элемент - %d\n", arr[0]) // получаем значение элемента массива, оно равно 42
fmt.Printf("Последний элемент - %d\n", arr[9]) // возвращается значение по умолчанию 10-го элемента этого массива, в данном случае оно равно 0.
Поскольку длина массива является составной частью его типа, [3]int
и [4]int
- разные типы. Поэтому длину массива менять нельзя. Когда массивы используются в качестве аргументов, функции работают с их копиями, не ссылками! Если Вы хотите работать со ссылками, используйте срезы
, о которых мы поговорим позже.
При определении массивов можно пользоваться коротким объявлением :=
.
a := [3]int{1, 2, 3} // определяем целочисленный массив длиной 3 элемента.
b := [10]int{1, 2, 3} // определяем целочисленный массив длиной 10 элементов, из которых первым трем присваиваем значения. Остальным элементам присваивается значение по умолчанию 0.
c := [...]int{4, 5, 6} // используйте `…` вместо значения длины, и Go посчитает ее за Вас.
Вам может захотеться использовать массивы в качестве элементов другого массива. Давайте посмотрим, как это делается:
// определяем двумерный массив, состоящий из 2 элементов, каждый из которых - массив, который содержит по 4 элемента.
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// То же самое более коротким способом:
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
Структура, лежащая в основе массива:
Рисунок 2.2 Отношения внутри многомерного массива
Часто тип 'массив' - не очень подходящий тип, например, когда при определении мы не знаем точно, какова будет длина массива. Поэтому нам нужен "динамический массив". В Go такой динамический массив называется срезом (slice)
.
Вообще срез
- это не динамический массив
. Это ссылочный тип. срез
указывает на лежащий в его основе массив
, его объявление аналогично объявлению массива
, но длина не указывается.
// так же, как мы объявляем массив, но здесь мы не указываем длину
var fslice []int
Так мы определяем срез
и задаем его начальное значение:
slice := []byte {'a', 'b', 'c', 'd'}
Срез
может переопределять существующие массивы и срезы. Срез
использует array[i:j]
, чтобы получить фрагмент массива array
, где i
- начальный индекс, а j
- конечный, но имейте в виду, что array[j]
не войдет в срез, так как длина среза равна j-i
.
// Определяем массив длиной 10 элементов, элементы являются значениями типа byte.
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// Определяем два среза типа []byte
var a, b []byte
// 'a' указывает на элементы с 3-го по 5-ый в массиве ar.
a = ar[2:5]
// теперь 'a' содержит ar[2],ar[3] и ar[4]
// 'b' - еще один срез массива ar
b = ar[3:5]
// теперь 'b' содержит ar[3] и ar[4]
Имейте в виду разницу между срезом
и массивом
, когда определяете их. Для вычисления длины массива используется […]
, но для определения среза - только []
.
Структура, лежащая в основе срезов:
Рисунок 2.3 Связь между срезом и массивом
Со срезами можно производить некоторые удобные операции:
- Первый элемент среза имеет индекс 0,
ar[:n]
равенar[0:n]
. - Если второй индекс элемента не указан, он равен длине среза,
ar[n:]
равенar[n:len(ar)]
. - Можно использовать
ar[:]
, чтобы срез был равен всему массиву, как следует из сказанного в двух предыдущих пунктах.
Еще примеры, относящиеся к срезам
:
// определяем массив
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// определяем два среза
var aSlice, bSlice []byte
// некоторые удобные операции
aSlice = array[:3] // то же, что и aSlice = array[0:3] aSlice содержит a,b,c
aSlice = array[5:] // то же, что и aSlice = array[5:10] aSlice содержит f,g,h,i,j
aSlice = array[:] // то же, что и aSlice = array[0:10] aSlice содержит все элементы
// срез из среза
aSlice = array[3:7] // aSlice содержит d,e,f,g,len=4 (длина),cap=7 (емкость)
bSlice = aSlice[1:3] // bSlice содержит aSlice[1], aSlice[2], или e,f
bSlice = aSlice[:3] // bSlice содержит aSlice[0], aSlice[1], aSlice[2], или d,e,f
bSlice = aSlice[0:5] // срез может быть расширен до значения емкости, теперь bSlice содержит d,e,f,g,h
bSlice = aSlice[:] // bSlice содержит все элементы aSlice или d,e,f,g
Срез
- ссылочный тип, поэтому при его изменении изменятся также значения всех остальных переменных, указывающих на тот же срез или массив. Например, возвращаясь к aSlice
и bSlice
, о которых шла речь выше, если изменить значение одного из элементов aSlice
, bSlice
тоже будет изменен.
Срез
похож на struct и состоит из 3 частей:
-
Указатель на то, где начинается
срез
. -
Длина
среза
. -
Емкость - длина от начального до конечного индексов
среза
.Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} Slice_a := Array_a[2:5]
Структура, лежащая в основе это кода:
Рисунок 2.4 Информация о срезе на основе массива
Срез имеет несколько встроенных функций:
len
возвращает длинусреза
.cap
возвращает максимальную длинусреза
append
присоединяет ксрезу
один или несколько элементов и возвращает новыйсрез
.copy
копирует элементы из одного среза в другой и возвращает количество скопированных элементов.
Внимание: append
изменяет массив, на который указывает срез
и затрагивает все остальные срезы, указывающие на тот же массив. Также, если срезу не хватает длины массива ((cap-len) == 0
), append
возвращает новый массив, на который теперь будет указывать этот срез. В этом случае значения других срезов, указывающих на старый массив, не изменятся.
Поведение карты (map)
похоже на то, как ведет себя словарь в Python. Чтобы определить карту, используйте map[типКлюча]типЗначения
.
Давайте посмотрим на пример кода. Команды изменения и получения значений для карты
похожи на соответствующие для среза
, однако индекс для среза
может быть только типа 'int', в то время как карта
может использовать для этих целей гораздо больше: например int
, string
или вообще все, что захотите. Также можно использовать ==
и !=
, чтобы сравнивать значения между собой.
// используем `string` для задания типа ключа, `int` для задания типа значения и инициализируем карту с помощью `make`.
var numbers map[string] int
// еще один способ определить карту
numbers := make(map[string]int)
numbers["one"] = 1 // задаем значение элементу по его ключу
numbers["ten"] = 10
numbers["three"] = 3
fmt.Println("Третий элемент равен: ", numbers["three"]) // получаем значения
// Код выводит: Третий элемент равен: 3
Несколько замечаний при использовании карт:
- элементы в
карте
неупорядоченны. Каждый раз, когда Вы печатаетекарту
, Вы получите различные результаты. Получить значения поиндексу
невозможно - следует использоватьключи
. - У
карты
нет фиксированной длины. Это ссылочный тип, как исрез
. len (длина)
работает также и скартой
. Она возвращает количествоключей
в карте.- Изменить значение в
карте
очень просто. Чтобы изменить значениеключа
one на11
, нужно использовать выражениеnumbers["one"]=11
.
Чтобы задать значения элементам карты, нужно использовать форму ключ:значение
, также у карты
есть встроенные методы для того, чтобы проверить, содержит ли она заданный ключ.
Для того, чтобы удалить элемент в карте
, используйте delete
.
// Задаем карте начальное значение
rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// карта возвращает два значения. В качестве второго, если элемента с таким ключом не существует, 'ok' принимает значение 'false', иначе - 'true'.
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# находится в карте, его рейтинг - ", csharpRating)
} else {
fmt.Println("Не можем найти рейтинг C# в карте")
}
delete(rating, "C") // удаляем элемент с ключом "C"
Как я уже говорил выше, карта
является ссылочным типом. Если две карты
указывают на один и тот же объект, любое его изменение затронет обе карты:
m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut" // теперь m["hello"] равняется Salut
make
выделяет память для объектов встроенных типов, таких как карта
, срез
, и канал
, в то время как new
служит для выделения памяти под сами типы.
new(T)
размещает в памяти нулевое значение типа T
и возвращает его адрес в памяти, типом которого является *T
. В терминах Go оно возвращает указатель на нулевое значение типа T
.
new
возвращает указатели.
У встроенной функции make(T, args)
другое предназначение, нежели у new(T)
. make
используется для slice(срезов)
, map(карт)
и channel(каналов)
и возвращает стартовое значение типа T
. Это делается потому, что данные для этих трех типов должны быть изначально проинициализированы перед тем, как на них указывать. Например, срез(slice)
содержит указатель, который указывает на лежащий в его основе array(массив)
, его длину и емкость. Пока эти данные не проинициализированы, значение slice
равно nil
, поэтому для slice
, map
и channel
, make
инициализирует лежащие в их основе данные и присваивает некоторые подходящие значения.
make
возвращает ненулевые значения.
Следующий рисунок показывает, как отличаются new
и make
.
Рисунок 2.5 Выделение памяти для данных, лежащих в основе в случае make и new
Нулевое значение - это не пустое значение. Это то значение, которое в большинстве случаев является значением по умолчанию для переменной соответствующего типа. Вот список нулевых значений для некоторых типов:
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 // по сути rune - это int32
byte 0x0 // по сути byte - это uint8
float32 0 // длина - 4 байта
float64 0 // длина - 8 байт
bool false
string ""
- Содержание
- Предыдущий раздел: "Hello, Go"
- Следующий раздел: Управляющие конструкции и функции