455 lines
31 KiB
Markdown
455 lines
31 KiB
Markdown
# 2.2 Основы Go
|
||
|
||
В этом разделе мы научим Вас тому, как определять константы, переменные, относящиеся к элементарным типам данных, а также некоторым приемам программирования на 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
|
||
|
||
Следующий рисунок приведен из статьи про [структуру данных в Go](http://research.swtch.com/godata) в [блоге Russ Cox](http://research.swtch.com/). Для хранения данных 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` (И так до тех пор, пока не встретится константа с явно указанным значением, после этого значения всех идущих после нее констант с явно неуказанным значением будут равны ее значению - прим. переводчика на русский).
|
||
|
||
### Перечисление 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
|
||
|
||
`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 ""
|
||
|
||
## Ссылки
|
||
|
||
- [Содержание](preface.md)
|
||
- Предыдущий раздел: ["Hello, Go"](02.1.md)
|
||
- Следующий раздел: [Управляющие конструкции и функции](02.3.md)
|