Перевод: Go Programming Language Tutorial: Learn Golang with Example

Перевод статьи Go Programming Language Tutorial: Learn Golang with Example

Что такое Go?

Go (также Golang) - язык программирования с открытым исходным кодом, разрабатываемый компанией Google. Он является статитически типизируемым компилируемым языком. Go поддерживает параллельное программирование, то есть он позволяет запуск нескольких процессов одновременно. Это достигается использованием каналов (channels), потоков (goroutines) и т.д. Go имеет сборщик мусора, который управляет памятью и позволяет отложенный запуск функций.

Как скачать и установить GO

Шаг 1) Перейти на https://golang.org/dl/. Скачать бинарник для вашей ОС.

Шаг 2) Дважды кликнуть на установщик и нажать Run

Шаг 3) Нажать Next

Шаг 4) Выбрать директории для установки и нажать Next

Шаг 5) Нажать Finish после завершения установки.

Шаг 6) После завершения установки вы можете проверить ее открыв терминал и написав

go version

Это покажет версию установленного Go

Ваша первая программа

Создайте директорию с названием studyGo. Вы создадите вашу Go программу в этой директории. Файлы Go создаются с раширением .go. Вы можете запускать программы Go используя синтакс

go run <filename>

Создайте файл с именем first.go, добавьте код ниже в него и сохраните

package main
import ("fmt")

func main() {
	fmt.Println("Hello World! This is my first Go program\n")
}

Перейдите в эту директорию в терминале. Запустите программу используя команду

go run first.go

Вы увидете выходную строку

Hello World! This is my first Go program

Теперь давайте обсудим программу выше.

package main - каждая программа Go должна начинаться с имени пакета. Go позволяет использовать в других программах Go и следовательно поддерживает переиспользование кода. Выполнение программы Go начинается с кода внутри пакета с именем main.

import fmt - импорт пакета fmt. Этот пакет обеспечивает выполнение I/O (ввод/вывод) функций.

func main() - это функция, с которой начинается выполнение программы. Функция main всегда должна быть расположена в пакете main. После main() вы можете писать код внутри фигурных скобок {}.

fmt.Println - это выведет текст на экран при помощи функции Println пакета fmt.

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

go run <filename>

Типы данных

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

В Go есть 3 основных типа.

Числовые типы - представляют из себя числовые значения, которые включают целые (integer), с плавающей точкой (floating) и комплексные (complex) значения. Варианты числовых типов:

int8 - 8 bit знаковое целое число.

int16 - 16 bit знаковое целое число.

int32 - 32 bit знаковое целое число.

int64 - 64 bit знаковое целое число.

uint8 - 8 bit беззнаковое целое число.

uint16 - 16 bit беззнаковое целое число.

uint32 - 32 bit беззнаковое целое число.

uint64 - 64 bit беззнаковое целое число.

float32 - 32 bit число с плавающей точкой.

float64 - 64 bit число с плавающей точкой.

complex64 - содержит 32 bit действительную и мнимую части.

complex128 - содержит 32 bit действительную и мнимую части.

Строковые типы - представляют из себя последовательность байт (символов). Вы можете делать различные операции над строками, такие как конкатенация, извлечение подстрок и т.д.

Логические типы - представляют из себя 2 значения, такие как true или false

Переменные

Переменные указывают на участок памяти, который хранит различные типы значений. Параметр type (см. синтакс ниже) представляет из себя тип значения, которое может быть сохранено в памяти.

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

var <variable> <type>

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

Вы можете также дать начальное значение переменной во время объявления самой себя используя синтакс

var <variable_name> <type> = <value>

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

var <variable_name> = <value>

Также вы можете объявлять сразу несколько переменных используя синтакс

 var <variable_name1>, <variable_name2>  = <value1>, <value2>

Программа ниже содержит некоторые примеры объявления переменных

package main
import "fmt"

func main() {
    //declaring a integer variable x
    var x int
    x=3 //assigning x the value 3 
    fmt.Println("x:", x) //prints 3
    
    //declaring a integer variable y with value 20 in a single statement and prints it
    var y int=20
    fmt.Println("y:", y)
    
    //declaring a variable z with value 50 and prints it
    //Here type int is not explicitly mentioned 
    var z=50
    fmt.Println("z:", z)
    
    //Multiple variables are assigned in single line- i with an integer and j with a string
    var i, j = 100,"hello"
    fmt.Println("i and j:", i,j)
}

Вывод будет следующим

x: 3
y: 20
z: 50
i and j: 100 hello

Go также предоставляет легкий способ объявления переменных со значением при помощи опускания ключевого слова var используя синтакс

 <variable_name> := <value>

Примечание: вы использовали := вместо =. Вы не можете использовать := для присваивания значения переменной которая уже объявлена. := используется для объявления переменной и присваивания ей значения.

Создайте файл с названием assign.go со следующим кодом

package main
import ("fmt")

func main() {
	a := 20
	fmt.Println(a)

	//gives error since a is already declared
	a := 30
	fmt.Println(a)
}

Выполните команду go run assign.go для просмотра результата

./assign.go:7:4: no new variables on left side of :=	

Переменные, объявленные без начального значения, будут иметь 0 для числовых типов, false для логических и пустую строку для строк.

Константы

Константы - такие переменные, значения которых не могут быть изменены. Константа в Go объяляется при помощи ключевого слова "const".

Создайте файл с именем constant.go и следующим кодом

package main
import ("fmt")

func main() {
	const b =10
	fmt.Println(b)
	b = 30
	fmt.Println(b)
}

Выполните команду go run constant.go чтобы увидель результат

.constant.go:7:4: cannot assign to b

Циклы

Циклы используются для выполнения блока выражений повторно, основанного на условии. Большинство языков программирования предоставляют 3 типа циклов: for, while и do while. Но Go поддерживает только цикл for.

Синтаксис цикла for:

for initialisation_expression; evaluation_expression; iteration_expression{
   // one or more statement
}

initialisation_expression выполняется первым (и только один раз)

Затем evaluation_expression проверяется и если оно равно true, то код выполняется код внутри блока.

Выполняется iteration_expression и evaluation_expression выполняется повторно. Если оно true, то код внутри цикла выполняется снова. Это будет продолжаться пока evaluation_expression не окажется false.

Скопируйте программу ниже в файл и выполните ее, чтобы увидель как цикл for выведет числа от 1 до 5:

package main
import "fmt"

func main() {  
var i int
for i = 1; i <= 5; i++ {
fmt.Println(i)
    }
}

Вывод:

1
2
3
4
5

If else

I else - это условное выражение. Синтасис следующий:

if condition{
// statements_1
}else{
// statements_2
}

Здесь выполняется условие condition и если оно true - то выпоняется выражение statement_1, иначе выполняется выражение statement_2.

Вы можете использовать условное выражение без else. Так же условное выражение может содержать последовательность идущих друг за другом if else. Программы ниже смогут объяснят лучше if else.

Выполните программу ниже. Она проверяет меньше ли 10-ти число x. Если это так - она выводит "x is less than 10":

package main
import "fmt"

func main() {  
    var x = 50
    if x < 10 {
        //Executes if x < 10
        fmt.Println("x is less than 10")
    } 
}

Т.к. значение x больше 10, то выражение внутри блока if не выполнится.


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

package main
import "fmt"

func main() {  
    var x = 50
    if x < 10 {
        //Executes if x is less than 10
        fmt.Println("x is less than 10")
    } else {
        //Executes if x >= 10
        fmt.Println("x is greater than or equals 10")
    }
}

Эта программа даст вам такой вывод

x is greater than or equals 10

Сейчас мы увидим программу с множественными if else блоками (идущими последовательно if else). Выполните пример ниже. Она проверяет число меньше ли 10 или между 10 и 90 или больше чем 90

package main
import "fmt"

func main() {  
    var x = 100
    if x < 10 {
        //Executes if x is less than 10
        fmt.Println("x is less than 10")
    } else if x >= 10 && x <= 90 {
        //Executes if x >= 10 and x<=90
        fmt.Println("x is between 10 and 90")
    } else {
        //Executes if both above cases fail i.e x>90
        fmt.Println("x is greater than 90")
    }
}

Здесь первое (сначала) if условие проверяет x меньше ли 10 и оно не меньше. Поэтому проверяется следующее условие (else if), где x между 10 и 90, и оно также ложно. Поэтому выполняется блок после ключевого слова else, который дает следующий вывод

x is greater than 90

Switch

Switch - это еще одно условное выражение. Выражения switch проверяют условие и результат сравнивается с набором имеющихся значений. Как только найдено соответствие, выполняется выражение ассоциироанное с этим соответствием. Если соответсвие не найдено, то ничего не будет выполнено. Вы также можете добвать блок по-умолчанию к switch который будет выполнен если другие соответствия не найдены. Синтаксис swith следующий:

switch expression {
    case value_1:
        statements_1
    case value_2:
        statements_2
    case value_n:
        statements_n
    default:
        statements_default
    }

Здесь значение выражения сравнивается с значениями в каждом блоке case. Как только найдено соответствие, выполняется выражение ассоциироанное с этим соответствием. Если соответствие не найдено, то выполняются выражения под секцей  default.

Выполните программу ниже:

package main
import "fmt"

func main() {  
    a,b := 2,1
    switch a+b {
    case 1:
        fmt.Println("Sum is 1")
    case 2:
        fmt.Println("Sum is 2")
    case 3:
        fmt.Println("Sum is 3")
    default:
        fmt.Println("Printing default")
    }
}

Вы получите следующий вывод:

Sum is 3	

Измените значение переменной a и b равное 3 и получите следующий результат:

Printing default

Вы можете также иметь множественные значения в блоке case, разделив их запятой.

Arrays (Массивы)

Массив представляет из себя фиксированный размер именованной последовательности элементов одного типа. Вы не можете иметь массив который содержит и числа и символы. Вы не можете размер массива однажды его задав.

Синтаксис объяления массива следующий:

var arrayname [size] type

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

arrayname [index] = value

Индекс массива начинается с нуля

Вы можете присваивать значения элементам массива во время объявления:

arrayname := [size] type {value_0,value_1,…,value_size-1} 

Вы можете также опустить параметр размера во время объявления массива со значениями при помощи замены size на ... и компилятор найдет размер массива на основе кол-ва элементов. Синстаксис следующий:

arrayname :=  […] type {value_0,value_1,…,value_size-1}

Вы можете найти размер массива рпи помощи след. синтаксиса:

len(arrayname)

Выполните пример ниже для понимания массива

package main
import "fmt"

func main() {  
    var numbers [3] string //Declaring a string array of size 3 and adding elements 
    numbers[0] = "One"
    numbers[1] = "Two"
    numbers[2] = "Three"
    fmt.Println(numbers[1]) //prints Two
    fmt.Println(len(numbers)) //prints 3
    fmt.Println(numbers) // prints [One Two Three]

    directions := [...] int {1,2,3,4,5} // creating an integer array and the size of the array is defined by the number of elements 
    fmt.Println(directions) //prints [1 2 3 4 5]
    fmt.Println(len(directions)) //prints 5

    //Executing the below commented statement prints invalid array index 5 (out of bounds for 5-element array)
    //fmt.Println(directions[5]) 
}

Вывод

Two
3
[One Two Three]
[1 2 3 4 5]
5

Slice (Срез)

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

Содержимое среза это на самом деле указатели на элементы массива. Это значит что если вы измените элемент в срезе, то будет изменен элемент в массиве.

Синтаксис создания среза следующий

var slice_name [] type = array_name[start:end]

Это создаст срез с именем slice_name на основе массива с именем array_name с индексами элементов начинающихся с start и заканчивающихся end-1.

Выпроните программу ниже. Программа создаст срез из массива и выведет его. Также вы можете видеть что изменение содержимого среза будет менять содержимое массива.

package main
import "fmt"

func main() {  
    // declaring array
    a := [5] string {"one", "two", "three", "four", "five"}
    fmt.Println("Array after creation:",a)

    var b [] string = a[1:4] //created a slice named b
    fmt.Println("Slice after creation:",b)

    b[0]="changed" // changed the slice data
    fmt.Println("Slice after modifying:",b)
    fmt.Println("Array after slice modification:",a)
}

Эта программа выведет следующее

Array after creation: [one two three four five]
Slice after creation: [two three four]
Slice after modifying: [changed three four]
Array after slice modification: [one changed three four five]

Есть конкретные функции, которые можно применить к срезам

len() - возвращает длинну среза

append () - она используется для добавления значения1 и значения2 в существующий срез

append() - добавляет slice2 в slice1

Выполните следующую программму

package main
import "fmt"

func main() {  
	a := [5] string {"1","2","3","4","5"}
	slice_a := a[1:3]
	b := [5] string {"one","two","three","four","five"}
	slice_b := b[1:3]

    fmt.Println("Slice_a:", slice_a)
    fmt.Println("Slice_b:", slice_b)
    fmt.Println("Length of slice_a:", len(slice_a))
    fmt.Println("Length of slice_b:", len(slice_b))

    slice_a = append(slice_a,slice_b...) // appending slice
    fmt.Println("New Slice_a after appending slice_b :", slice_a)
    
    slice_a = append(slice_a,"text1") // appending value
    fmt.Println("New Slice_a after appending text1 :", slice_a)
}

Вывод будет

Slice_a: [2 3]
Slice_b: [two three]
Length of slice_a: 2
Length of slice_b: 2
New Slice_a after appending slice_b : [2 3 two three]
New Slice_a after appending text1 : [2 3 two three text1]

Программма первым делом создает 2 среза и выводит их длинну. Затем она добавила один срез к другому и добавила строку к получившемуся срезу

Функции

Функция представляет из себя блок выражений который выполняет особую задачу. Объявление функции сообщает нам имя функции, возвращаемый тип и входные параметры. Определение функции представляет из себя код внутри функции. Синтаксис объявления функции следующий

func function_name(parameter_1 type, parameter_n type) return_type {
//statements
}

Параметры и возвращаемый тип не обязательны. Также вы можете возвращать множественные значения из функции.

Давайте запустим следующий пример. Здесь функция, названная calc, будет принимать 2 числа и выполнять сложение и вычитание и возвращать два значения

package main
import "fmt"

//calc is the function name which accepts two integers num1 and num2
//(int, int) says that the function returns two values, both of integer type.
func calc(num1 int, num2 int)(int, int) {  
    sum := num1 + num2
    diff := num1 - num2
    return sum, diff
}

func main() {  
    x,y := 15,10

    //calls the function calc with x and y an d gets sum, diff as output
    sum, diff := calc(x,y) 
    fmt.Println("Sum",sum)
    fmt.Println("Diff",diff) 
}

Вывод будет

Sum 25
Diff 5

Пакеты

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

import package_name

Мы увидим и обсудим как создавать и использовать пакеты в следующем примере

Шаг 1) Создайте файл с именем package_example.go и добавьте в него код ниже

package main
import "fmt"
//the package to be created
import "calculation"

func main() {  
	x,y := 15,10
	//the package will have function Do_add()
sum := calculation.Do_add(x,y)
fmt.Println("Sum",sum) 
}

В программе выше fmt  - это пакет, который предоставляется нам главным образом для целей ввода/вывода. Также вы можете увидеть пакет с именем calculation. Внутри main() вы можете видеть шаг sum := calculation.Do_add(x,y). Это значит вы вызываете функцию Do_add из пакета calculation.

Шаг 2)  Первым делом вы должны создать пакет calculation внутри папки с таким же именем в src папке go. Путь к установленному Go можно найти в переменной окружения PATH.

На mac'e найти путь можно при помощи выполнения команды echo $PATH

В windows найти путь можно при помощи команды echo %GOROOT%

Шаг 3) Перейдите в директорию src (/usr/local/go для mac и C:\Go\src для windows). Исходя из кода выше имя пакета должно быть calculation. Go требует чтобы пакет располагался в одноименной директории в src. Создайте директорию с именем calculation в директории src.

Шаг 4) Создайте файл с именем calc.go (Вы можете дать другое имя, но имя пакета имеет важное значение в коде. Здесь оно должно быть calculation) в директории calculation и добавьте следующий код

package calculation
  
func Do_add(num1 int, num2 int)(int) {
    sum := num1 + num2
    return sum
}

Шаг 5) Выполните команду go install из директории calculation, которая скомпилирует calg.go.

Шаг 6) Теперь вернитесь к package_example.go и выполните go run package_example.go. В выводе вы получите: Sum 25.

Обратите внимание: имя функции Do_add начинается с заглавной буквы. Это потому что в Go если имя функции начинается с заглавной буквы, то другие программы могут видеть ее, иначе другие программы не могут получить к ней доступ. Если имя функции было бы do_add, то вы бы получили ошибку

cannot refer to unexported name calculation.calc..

Defer and stacking defers

Отложенные выражения используются для отложенного вызова функции до тех пор пока функция содержащая это отложенное выражение не завершит выполнение.

Давайте изучим это на примере:

package main
import "fmt"

func sample() {  
    fmt.Println("Inside the sample()")
}
func main() {  
    //sample() will be invoked only after executing the statements of main()
    defer sample()
    fmt.Println("Inside the main()")
}

Вывод будет:

Inside the main()
Inside the sample()

Здесь выполнение функции sample() отложено до тех пор пока не завершит выполнение главная функция (main).


Stacking defer использует множественные отложенные выражения. Представьте что вы имеете несколько вызовов defer внутри функции. Go располагает все отложенные функции в стеке, и как только главная функция отработает, функции из стека выполнятся в порядке Последним пришел - первым выполнился (LIFO). Вы можете увидеть это в примере ниже.

Выполните код ниже

package main
import "fmt"

func display(a int) {  
    fmt.Println(a)
}
func main() {  
    defer display(1)
    defer display(2)
    defer display(3)
    fmt.Println(4)
}

Вывод будет

4
3
2
1

Здесь код внутри main() выполняется первым, и затем вызываются отложеныне функции в обратном порядке, например, 4, 3, 2, 1.

Указатели

Перед объяснением указателей давайте сначала обсудим "&" оператор. Оператор "&" используется для получения адрема переменной. Это значит что "&a" напечатает адрес памяти переменной a.

Выполните программу ниже для вывода значения переменной и адреса этой переменной

package main
import "fmt"

func main() {
	a := 20
	fmt.Println("Address:",&a)
	fmt.Println("Value:",a)
}

Результат будет

Address: 0xc000078008
Value: 20

Переменная-указатель хранит адрес памяти другой переменной. Вы можете объявить указатель используя синтаксис

var variable_name *type

Звездочка (*) представляет из себя переменную как указатель. Вы можете понять больше из следующего примера

package main
import "fmt"

func main() {
	//Create an integer variable a with value 20
	a := 20
	
	//Create a pointer variable b and assigned the address of a
	var b *int = &a

	//print address of a(&a) and value of a  
	fmt.Println("Address of a:",&a)
	fmt.Println("Value of a:",a)

	//print b which contains the memory address of a i.e. &a
	fmt.Println("Address of pointer b:",b)

	//*b prints the value in memory address which b contains i.e. the value of a
	fmt.Println("Value of pointer b",*b)

	//increment the value of variable a using the variable b
	*b = *b+1

	//prints the new value using a and *b
	fmt.Println("Value of pointer b",*b)
	fmt.Println("Value of a:",a)
}

Вывод будет

Address of a: 0x416020
Value of a: 20
Address of pointer b: 0x416020
Value of pointer b 20
Value of pointer b 21
Value of a: 21

Структуры

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

Использование стуктуры состоит из 2-х шагов.

Первый - создание (объявление) типа структуры.

Второй - создание переменной этого типа для сохранения значений.

Структуры главным образом используются когда вы хотите хранить связанные данные вместе.

Рассмотрим часть информации о сотруднике, где есть имя, возраст и адрес. Вы можете пойти двумя путями.

Создать три массива, в первом хранить имена, во втором возраст и в третьем - адреса.

Объявите тип структуры с тремя полями: имя, адрес и возраст. Создайте массив с этим типом структуры, где каждый элемент объект структуры, имеющий имя, адрес и возраст.

Первый подход не является эффективным. В этих сценариях структуры более удобны.

Синтаксис объявления структуры будет

type structname struct {
   variable_1 variable_1_type
   variable_2 variable_2_type
   variable_n variable_n_type
}

Пример объявления структуры

type emp struct {
    name string
    address string
    age int
}

Здесь новый определенный пользователем тип назван emp. Сейчас вы можете создать переменные с типом emp используя синтаксис

var variable_name struct_name

Пример

var empdata1 emp 

Вы можете присвоить значения для переменной empdata1

empdata1.name = "John"
	empdata1.address = "Street-1, Bangalore"
	empdata1.age = 30

Вы можете также создать структурную переменную и присвоить значения при помощи

empdata2 := emp{"Raj", "Building-1, Delhi", 25}

Здесь вам нужно соблюдать порядок элементов. Raj будет соотносено с name, следующий элемент с address и последний с age.

Выполните код ниже

package main
import "fmt"

//declared the structure named emp
type emp struct {
        name string
        address string
        age int
}       

//function which accepts variable of emp type and prints name property
func display(e emp) {
          fmt.Println(e.name)
}

func main() {
// declares a variable, empdata1, of the type emp
var empdata1 emp
//assign values to members of empdata1
empdata1.name = "John"
empdata1.address = "Street-1, London"
empdata1.age = 30

//declares and assign values to variable empdata2 of type emp
empdata2 := emp{"Raj", "Building-1, Paris", 25}

//prints the member name of empdata1 and empdata2 using display function
display(empdata1)
display(empdata2)
}

Вывод будет

John
Raj

Методы (не функции)

Метод это функция с аргументом - получателем. Архитектурно это между ключевым сломов func и именем метода. Синтаксис следующий

func (variable variabletype) methodName(parameter1 paramether1type) {  
}

Давайте преобразуем пример выше для использования метода вместо функции

package main
import "fmt"

//declared the structure named emp
type emp struct {
    name string
    address string
    age int
}

//Declaring a function with receiver of the type emp
func(e emp) display() {
    fmt.Println(e.name)
}

func main() {
    //declaring a variable of type emp
    var empdata1 emp
    
    //Assign values to members
    empdata1.name = "John"
    empdata1.address = "Street-1, Lodon"
    empdata1.age = 30

    //declaring a variable of type emp and assign values to members
    empdata2 := emp {
        "Raj", "Building-1, Paris", 25}

    //Invoking the method using the receiver of the type emp
   // syntax is variable.methodname()
    empdata1.display()
    empdata2.display()
}

Го не объектно-ориентированный язык и он не имеет такого понятия как класс. Методы дают ощущение того что вы делаете в объектно-ориентированных программах где функции классов выполняются при помощи использования синтаксиса objectname.functionname()

Многопоточность

Го поддерживает многопоточное выполнение задач. Это значит что Go может выполнять множественные задачи одновременно. Это отличается от понятия параллельности. В параллелизме задача разбивается на маленькие подзадачи и они выполняются параллельно. Но в многопоточности множественные задачи выполняются одновременно. Многопоточность достигается в Go использованием Горутин и каналов.

Горутины

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


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

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

Горутина запускается используя ключевое слово go с последующим вызовом функции.

Пример

go add(x,y)

Вы поймете горутины в примере ниже. Выполните код программы:

package main
import "fmt"
    
func display() {
	for i:=0; i<5; i++ {
		fmt.Println("In display")
	}
}

func main() {
	//invoking the goroutine display()
	go display()
	//The main() continues without waiting for display()
	for i:=0; i<5; i++ {
		fmt.Println("In main")
	}
}

Вывод будет:

In main
In main
In main
In main
In main

Здесь главная программа завершила выполнение даже перед тем как была запущена горутина. Функция display() и есть горутина, которая запускается используя синтаксис:

go function_name(parameter list)

В коде выше функция main() не ждет когда завершится функция display() и функция main() будет завершена перед тем как функция display() выполнит свой код. И так, фыражение print внутри display() не выведет ничего в output.

Сейчас мы доработаем программу для печати результата функции display() тоже. Мы добавим задержку времени в 2 секунды в цикле for и 1 секундную задержку в функции display()

package main
import "fmt"
import "time"
    
func display() {
	for i:=0; i<5; i++ {
		time.Sleep(1 * time.Second)
		fmt.Println("In display")
	}
}

func main() {
	//invoking the goroutine display()
	go display()
	for i:=0; i<5; i++ {
		time.Sleep(2 * time.Second)
		fmt.Println("In main")
	}
}

Вывод будет похож на этот:

In display
In main
In display
In display
In main
In display
In display
In main
In main
In main

Здесь вы можете видеть два цикла начали выполнение параллельно потому что используется многопоточность.

Каналы

Каналы - это способ взаимодействия функций друг с другом. Они могут восприниматься как среда куда одна горутина кладет данные и доступная другим рутинам.

Канал может быть объявлен так:

channel_variable := make(chan datatype)

Пример:

ch := make(chan int)

Вы можете отправить данные в канал используя синтаксис:

channel_variable <- variable_name

Пример:

 ch <- x

Вы можете получить данные из канала используя синтаксис:

variable_name := <- channel_variable

Пример:

y := <- ch

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

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

Выполните программу ниже

package main
import "fmt"
import "time"
    
func display() {
	time.Sleep(5 * time.Second)
	fmt.Println("Inside display()")
}

func main() {
	go display()
	fmt.Println("Inside main()")
}

Вывол будет

Inside main()

Функция main() завершила выполнение и вышла перед выполнением горутины. Таким образом печать внутри display() не выполнится.

Сейчас доработайте программу выше для использования каналов и посмотрите поведение

package main
import "fmt"
import "time"
    
func display(ch chan int) {
	time.Sleep(5 * time.Second)
	fmt.Println("Inside display()")
	ch <- 1234
}

func main() {
	ch := make(chan int) 
	go display(ch)
	x := <-ch
	fmt.Println("Inside main()")
	fmt.Println("Printing x in main() after taking from channel:",x)
}

Вывод будет

Inside display()
Inside main()
Printing x in main() after taking from channel: 1234

Что здесь происходит: функция main() достигая x := <-ch будет ждать данные канала. Функция display() ждет 5 секунд и затем отправляет данные в канал. Main() получив данные из канала разблокируется и продолжит выполнение. 

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

close(channel_name)

И на стороне получателя можно проверить закрыл ли канал используя дополнительную переменную при получении данных из канала использую

variable_name, status := <- channel_variable

Если статус равен True - это значит что вы получили данные из канала. Если статус False - вы пытаетесь прочитать данные из закрытого канала.

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

package main
import "fmt"
import "time"

//This subroutine pushes numbers 0 to 9 to the channel and closes the channel
func add_to_channel(ch chan int) {	
	fmt.Println("Send data")
	for i:=0; i<10; i++ {
		ch <- i //pushing data to channel
	}
	close(ch) //closing the channel

}

//This subroutine fetches data from the channel and prints it.
func fetch_from_channel(ch chan int) {
	fmt.Println("Read data")
	for {
		//fetch data from channel
x, flag := <- ch

		//flag is true if data is received from the channel
//flag is false when the channel is closed
if flag == true {
			fmt.Println(x)
		}else{
			fmt.Println("Empty channel")
			break	
		}	
	}
}

func main() {
	//creating a channel variable to transport integer values
	ch := make(chan int)

	//invoking the subroutines to add and fetch from the channel
	//These routines execute simultaneously
	go add_to_channel(ch)
	go fetch_from_channel(ch)

	//delay is to prevent the exiting of main() before goroutines finish
	time.Sleep(5 * time.Second)
	fmt.Println("Inside main()")
}

Здесь есть две горуины: одна отправляет данные в канал, другая распечатывает данные из канала. Функция add_to_chamnnel добавляет числа от 0 до 9 и закрывает канал. Одновременно функция fetch_from_channel ждет строку x, flag := <- ch и как только данные появятся они напечатаются. Она выйдет как только переменная flag станет false, что значит канал закрыт.

Ожидание в main() дается для предотвращения выхода из min() до тех пор пока горутины не завершат выполнение.

Выполните код и посмотри на вывод

Read data
Send data
0
1
2
3
4
5
6
7
8
9
Empty channel
Inside main()

 

Оператор Select

Select может быть рассмотрен как оператор switch который работает с каналами. Здесь выражения case будут операциями каналов. Обычно, каждое case выражение будет попыткой считывания из канала. Когда какой-либо из кейсов готов (канал прочитан), тогда выражение, связанное с этим кейсом, выполняется. Если одновременно несколько кейсов готовы, то будет выбран один случайный. Вы можете иметь case по-умолчанию, который будет выполнен если ни один из кейсов не готов.

Давайте посмотрим на код ниже:

package main
import "fmt"
import "time"

//push data to channel with a 4 second delay
func data1(ch chan string) {  
    time.Sleep(4 * time.Second)
    ch <- "from data1()"
}

//push data to channel with a 2 second delay
func data2(ch chan string) {  
    time.Sleep(2 * time.Second)
    ch <- "from data2()"
}

func main() {
    //creating channel variables for transporting string values
    chan1 := make(chan string)
    chan2 := make(chan string)
    
    //invoking the subroutines with channel variables
    go data1(chan1)
    go data2(chan2)
    
    //Both case statements wait for data in the chan1 or chan2.
    //chan2 gets data first since the delay is only 2 sec in data2().
    //So the second case will execute and exits the select block
    select {
    case x := <-chan1:
        fmt.Println(x)
    case y := <-chan2:
        fmt.Println(y)
    }
}

Выполнение программы выше даст следующий результат

from data2()

Здесь select выражение ждет когда данные будут доступны в любом из каналов. data2() добавляет данные в канал после 2 секунд остановки, которая вызовет выполнение второго варинта.

Добавьте default case в select в туже программу и посмотрите на результат. Здесь, достигнув блока select, если ни один из вариантов не имеет данных готовых для получения из канала, программа запустит default блок без ожидания появления данных в каком-нибудь канале.

package main
import "fmt"
import "time"

//push data to channel with a 4 second delay
func data1(ch chan string) {  
    time.Sleep(4 * time.Second)
    ch <- "from data1()"
}

//push data to channel with a 2 second delay
func data2(ch chan string) {  
    time.Sleep(2 * time.Second)
    ch <- "from data2()"
}

func main() {
    //creating channel variables for transporting string values  
    chan1 := make(chan string)
    chan2 := make(chan string)
    
    //invoking the subroutines with channel variables
    go data1(chan1)
    go data2(chan2)

    //Both case statements check for data in chan1 or chan2.
    //But data is not available (both routines have a delay of 2 and 4 sec)
    //So the default block will be executed without waiting for data in channels.
    select {
    case x := <-chan1:
        fmt.Println(x)
    case y := <-chan2:
        fmt.Println(y)
    default:
    	fmt.Println("Default case executed")
    }
}

Эта программа даст следующий вывод

Default case executed

Это потому что когда select блок достигнут, ни один из каналов не имеет данных для чтения. Следовательно, выполнится default case.

Мьютекс

Мьютекс - это сокращение от mutual (взаимный) и exclusion (исключение). Мьютекс используется когда вы не хотите давайть доступ к ресурсу нескольким горутинам одновременно. Мьютекс имеет два метода: Lock и Unlock. Мьютекс расположен в пакете sync. Следовательно вы должны иметь импорт этого пакета sync. Выражения которые должны взаимоисключаться должны быть расположены внутри mutex.Lock() и mutex.Unlock().

Давайте изучим мьютекс с примером, который считает кол-во выполнения итераций цикла. В этой программе мы ожидаем что рутина запустит цикл 10 раз и кол-во сохранит  результат в виде суммы. Вы запускаете эту рутину 3 раза, общий счет должен быть 30. Кол-во сохраняется в глобальной переменной count.

Сначала запустите программу без мьютекса

package main
import "fmt"
import "time"
import "strconv"
import "math/rand"
//declare count variable, which is accessed by all the routine instances
var count = 0

//copies count to temp, do some processing(increment) and store back to count
//random delay is added between reading and writing of count variable
func process(n int) {
	//loop incrementing the count by 10
	for i := 0; i < 10; i++ {
		time.Sleep(time.Duration(rand.Int31n(2)) * time.Second)
		temp := count
		temp++
		time.Sleep(time.Duration(rand.Int31n(2)) * time.Second)
		count = temp
	}
	fmt.Println("Count after i="+strconv.Itoa(n)+" Count:", strconv.Itoa(count))
}

func main() {
	//loop calling the process() 3 times
	for i := 1; i < 4; i++ {
		go process(i)
	}

	//delay to wait for the routines to complete
	time.Sleep(25 * time.Second)
	fmt.Println("Final Count:", count)
}

Смотри результат

Count after i=1 Count: 11
Count after i=3 Count: 12
Count after i=2 Count: 13
Final Count: 13

Результат может отличаться при каждом запуске, но он все-равно не будет равен 30.

Что здесь происходит если 3 горутины пытаются увеличить сохраненное кол-во итераций цикла в переменной count. Предположим в момент времени count равен 5 и горутина 1 собирается увеличить count до 6. Основные шаги включают:

копирование переменной count в temp

увеличение на 1 temp

сохранение переменной temp обратно в count

Предположим вскоре после выполнения шага 3 горутиной 1:

  • другая горутина может иметь старое значение, например, 3
  • выполняются шаги, описанные выше
  • сохраняется 4 обратно

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

Сейчас вы выполните программу с мьютексом. Здесь упомянутые выше 3 шага выполняются в мьютексе.

package main
import "fmt"
import "time"
import "sync"
import "strconv"
import "math/rand"

//declare a mutex instance
var mu sync.Mutex

//declare count variable, which is accessed by all the routine instances
var count = 0

//copies count to temp, do some processing(increment) and store back to count
//random delay is added between reading and writing of count variable
func process(n int) {
	//loop incrementing the count by 10
	for i := 0; i < 10; i++ {
		time.Sleep(time.Duration(rand.Int31n(2)) * time.Second)
		//lock starts here
		mu.Lock()
		temp := count
		temp++
		time.Sleep(time.Duration(rand.Int31n(2)) * time.Second)
		count = temp
		//lock ends here
		mu.Unlock()
	}
	fmt.Println("Count after i="+strconv.Itoa(n)+" Count:", strconv.Itoa(count))
}

func main() {
	//loop calling the process() 3 times
	for i := 1; i < 4; i++ {
		go process(i)
	}

	//delay to wait for the routines to complete
	time.Sleep(25 * time.Second)
	fmt.Println("Final Count:", count)
}

Теперь вывод будет

Count after i=3 Count: 21
Count after i=2 Count: 28
Count after i=1 Count: 30
Final Count: 30

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

Обработка ошибок

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

Пример ниже объяснит больше об ошибках

package main
import "fmt"
import "os"

//function accepts a filename and tries to open it.
func fileopen(name string) {
    f, er := os.Open(name)

    //er will be nil if the file exists else it returns an error object  
    if er != nil {
        fmt.Println(er)
        return
    }else{
    	fmt.Println("file opened", f.Name())
    }
}

func main() {  
    fileopen("invalid.txt")
}

Вывод будет

open /invalid.txt: no such file or directory

Здесь мы пытаемся открыть несуществующий файл и в переменной er вернется ошибка. Если файл существует, то ошибка будет пустой.

Пользовательские ошибки

Используя этот функционал, вы можете создавать пользовательские ошибки. Это достигается при помощи функции New() из пакета error. Мы доработаем программу выше для использования в ней пользовательских ошибок.

Выполните программу ниже:

package main
import "fmt"
import "os"
import "errors"

//function accepts a filename and tries to open it.
func fileopen(name string) (string, error) {
    f, er := os.Open(name)

    //er will be nil if the file exists else it returns an error object  
    if er != nil {
        //created a new error object and returns it  
        return "", errors.New("Custom error message: File name is wrong")
    }else{
    	return f.Name(),nil
    }
}

func main() {  
    //receives custom error or nil after trying to open the file
    filename, error := fileopen("invalid.txt")
    if error != nil {
        fmt.Println(error)
    }else{
    	fmt.Println("file opened", filename)
    }  
}

Вывод будет

Custom error message:File name is wrong

Чтение файлов

Файлы используются для сохранения данных. Go позволяет нам читать данные из файлов.

Сначала создайте файл data.txt в текущей директории со следующим содержимым

Line one
Line two
Line three

Сейчас запустите программу ниже чтобы увидеть как она выведет содержимое всего файла.

package main
import "fmt"
import "io/ioutil"

func main() {  
    data, err := ioutil.ReadFile("data.txt")
    if err != nil {
        fmt.Println("File reading error", err)
        return
    }
    fmt.Println("Contents of file:", string(data))
}

Здесь в строке data, err := ioutil.ReadFile("data.txt") читаются данные и возвращается последовательность байтов. Перед печаться данные конвеертируются в строку.

Запись в файлы

Вы ознакомитесь с записью в программе

package main
import "fmt"
import "os"

func main() {  
    f, err := os.Create("file1.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    l, err := f.WriteString("Write Line one")
    if err != nil {
        fmt.Println(err)
        f.Close()
        return
    }
    fmt.Println(l, "bytes written")
    err = f.Close()
    if err != nil {
        fmt.Println(err)
        return
    }
}

Здесь создается файл с именем test.txt. Если файл уже существует то его содержимое будет удалено. Функция Writeline() используется для записи контента в файл. После этого вы закрываете файл используя функцию Close().

Шпаргалка

В этой инструкции вы прошли:

 

продолжение следует...

Комментарии


Отправить комментарий