Skip to content

Latest commit

 

History

History
454 lines (298 loc) · 31.1 KB

File metadata and controls

454 lines (298 loc) · 31.1 KB

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 в блоге 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 (И так до тех пор, пока не встретится константа с явно указанным значением, после этого значения всех идущих после нее констант с явно неуказанным значением будут равны ее значению - прим. переводчика на русский).

Перечисление 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  ""

Ссылки