Основы 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 в блоге Russ Cox. Для хранения данных 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
.
Массивы, срезы, карты
Массив
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}}
Структура, лежащая в основе массива:
Срезы
Часто тип 'массив' - не очень подходящий тип, например, когда при определении мы не знаем точно, какова будет длина массива. Поэтому нам нужен "динамический массив". В 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]
Имейте в виду разницу между срезом
и массивом
, когда определяете их. Для вычисления длины массива используется […]
, но для определения среза - только []
.
Структура, лежащая в основе срезов:
Со срезами можно производить некоторые удобные операции:
- Первый элемент среза имеет индекс 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]
Структура, лежащая в основе это кода:
Срез имеет несколько встроенных функций:
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
.
У встроенной функции make(T, args)
другое предназначение, нежели у new(T)
. make
используется для slice(срезов)
, map(карт)
и channel(каналов)
и возвращает стартовое значение типа T
. Это делается потому, что данные для этих трех типов должны быть изначально проинициализированы перед тем, как на них указывать. Например, срез(slice)
содержит указатель, который указывает на лежащий в его основе array(массив)
, его длину и емкость. Пока эти данные не проинициализированы, значение slice
равно nil
, поэтому для slice
, map
и channel
, make
инициализирует лежащие в их основе данные и присваивает некоторые подходящие значения.
make
возвращает ненулевые значения.
Следующий рисунок показывает, как отличаются new
и make
.
Нулевое значение - это не пустое значение. Это то значение, которое в большинстве случаев является значением по умолчанию для переменной соответствующего типа. Вот список нулевых значений для некоторых типов:
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
- Следующий раздел: Управляющие конструкции и функции