Info
Content

Part 1: Getting Started with Go

CHAPTER 1: Hello, Go!

Используется версия Go 1.15.8

$ /usr/local/go/bin/go version
go version go1.15.8 darwin/amd64

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

package main
import "fmt"
func main() {
    fmt.Println("Hello, world!")
}

После сохранения ее нужно скомпилировать (так ты сможешь ее запустить)
Если передавать имя файла в go build, то имя бинарника будет таким же как имя исходного файла с кодом:

? /usr/local/go/bin/go build main.go
? tree
.
├── main
└── main.go

0 directories, 2 files

Запускаем, работает:

? ./main
Hello, World!

Имя полученного бинаря не связано с именем пакета объявленным в коде package main


Команда go run билдит и запускает код

? /usr/local/go/bin/go run .
Hello, World!

Первая строка нашего кода описывает имя пакета для нашей программы - package main
Программы на Go организуются в пакеты. Пакет это коллекция файлов сгруппированных в одной директории
В нашем примере main это имя пакета который хранится в директории ~/golang-study:

? cat main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}
? pwd
/Users/vandud/golang-study

Если у тебя будут дополнительные .go-файлы в этой же директории, то все они будут пренадлежать пакету main

Ты можешь использовать любое имя пакета, но пакет main имеет особое значение - в нем должна быть объявлена функция main(), которая является входной точкой для твоей программы

Имя пакета и имя файла также не обязаны быть одинаковыми

Следующая строка импортирует пакет fmt в нашу программу (этот пакет содержит различные функции позволяющие форматировать и выводить текст в консоль и читать ввод из нее)

Далее описывается функция main(), внутри которой вызывается функция Println() из пакета fmt, она выводит на экран строку "Hello, world!"


Добавим файл show_time.go со следующим содержимым:

? cat show_time.go
package main

import (
  "fmt"
  "time"
)

func displayTime() {
  fmt.Println(time.Now())
}

Так как функция displayTime() пренадлежит пакету main, то мы можем вызвать ее в файле main.go без дополнительных действий

? cat main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
  displayTime()
}

Ты можешь вызывать функции в рамках пакета без импорта этого же пакета

Так как теперь у нас два файла в пакете main, то не выйдет собрать программу указав команде go build какой-то файл

? /usr/local/go/bin/go build main.go
# command-line-arguments
./main.go:7:3: undefined: displayTime

Надо запустить go build без указания файла

? /usr/local/go/bin/go build
(yc-test:argocd) vandud@macbook: golang-study ? pwd
/Users/vandud/golang-study
(yc-test:argocd) vandud@macbook: golang-study ? tree
.
├── golang-study
├── main.go
└── show_time.go

0 directories, 3 files

Как мы видим, полученный бинарный файл имеет имя директории в которой лежали файлы

Программа работает

? ./golang-study
Hello, World!
2022-12-04 18:56:13.176349 +0300 MSK m=+0.000083751

Утилита go проставляет ряд переменных окружения, автоматически определяя их значения на основе окружающей среды, которые нужны для корректной работы твоих программ на Golang

? go env
GO111MODULE=""
GOARCH="arm64"
GOBIN="/usr/local/bin"
GOCACHE="/Users/vandud/Library/Caches/go-build"
GOENV="/Users/vandud/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="arm64"
GOHOSTOS="darwin"
...

Например переменные GOHOSTARCH="arm64", GOHOSTOS="darwin" используются для указания целевой архитектуры и ОС при компилировании программы

Чтобы скомпилировать программу под иную ОС или архитектуру нужно указать соответствующие значения в переменных GOOS и GOARCH

Operating Systems GOOS GOARCH
macOS darwin amd64
Linux linux amd64
Windows windows amd64

Заиспользовать их можно например так:

$ GOOS=darwin GOARCH=amd64 go build -o golang-study
? GOOS=darwin GOARCH=amd64 /usr/local/go/bin/go build -o golang-study
? file golang-study
golang-study: Mach-O 64-bit executable x86_64

? GOOS=linux GOARCH=amd64 /usr/local/go/bin/go build -o golang-study
? file golang-study
golang-study: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=d-Bqt3BFyEwGcvOg8GGx/JDswl2e8I5fpFwl-UhOJ/HcqndTyYXsMLIQOrr1pb/q7SVvy9uVEhV1Xa1o6zG, not stripped

? GOOS=windows GOARCH=amd64 /usr/local/go/bin/go build -o golang-study
? file golang-study
golang-study: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows

Говорят что Go обладает скоростью работы C и продуктивностью Python

В плане синтаксиса Go ближе к C и Java в которых используются фигурные скобки для блоков
Как и в Python, функции в Go это граждане первого класса, в то время как в Java все крутится вокруг классов
Но в отличие от Python и Java, Go не поддерживает ООП и наследование. Но он имеет интерфейсы и структуры, которые работают как классы
Также Go статически типизорованный как Java

В то время как Java и Python компилируются в байт код, который потом запускается на виртуальной машине, Go компилируется сразу в машинный код, что позволяет ему взять первенство в плане производительности
Как Java и Python, Go использует GC (garbage collection). GC это форма автоматического менеджмента памяти. Garbage Collector пытается высвободить память занятую объектами которые больше не используются в программе
Python ест много памяти, Java не сильно лучше, а Go дает больше контроля в плане управления памятью

Parallelism и Concurrency встроены в Go, это значит что писать мульти-поточные приложения очень просто (в Java и Python они тоже есть, но работают не так эффективно как в Go)

В плане библиотек: у Go их конечно меньше, так как язык еще молодой, но их количество быстро растет

CHAPTER 2: Working with Different Data Types

В Go есть 4 типа данных:

  • Basic - Examples include strings, numbers, and Booleans
  • Aggregate - Examples include arrays and structs
  • Reference - Examples include pointers, slices, functions, and channels
  • Interface - An interface is a collection of method signatures

Описывать переменные в Go можно несколькими способами
Первый из них - использование ключевого слова var перед именем переменной

var num1 = 5 // type inferred

Все что идет после двойного слэша - комментарий

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

? cat main.go
package main

import (
    "fmt"
    "reflect"
)


func main() {
  var myVar = 5
  var myString = "test string"
  fmt.Println("Type of 'myVar' variable: " + reflect.TypeOf(myVar).String())
  fmt.Println("Type of 'myString' variable: " + reflect.TypeOf(myString).String())
}
? /usr/local/go/bin/go run main.go
Type of 'myVar' variable: int
Type of 'myString' variable: string

Если объявить переменную но не заиспользовать ее в коде, то такая программа не скомпилируется

? cat main.go
package main

import "fmt"
import "reflect"

func main() {
  var myVar = 5
  var myString = "test string"
  fmt.Println("Type of 'myVar' variable: " + reflect.TypeOf(myVar).String())
}
? /usr/local/go/bin/go run main.go
# command-line-arguments
./main.go:8:7: myString declared but not used

Переменная может быть описана вне функции, тогда она будет доступна всем функциям:

? cat main.go
package main

import "fmt"

var myVar = "test string"

func main() {
  myFunc()
}

func myFunc() {
  fmt.Println(myVar)
}
? /usr/local/go/bin/go run main.go
test string

Если описана, но не инициализирована - ее значение будет равно нулевому значению выбранного типа данных
У каждого типа данных свое нулевое значение
Некоторые из них приведены ниже:

? cat main.go
package main

import "fmt"

func main() {
  var name9 int
  var name10 uint
  var name11 rune
  var name12 byte
  var name13 uintptr
  var name14 float32
  var name16 complex64
  var name18 bool
  var name19 string

  fmt.Println(name9)
  fmt.Println(name10)
  fmt.Println(name11)
  fmt.Println(name12)
  fmt.Println(name13)
  fmt.Println(name14)
  fmt.Println(name16)
  fmt.Println(name18)
  fmt.Println(name19)
}
? /usr/local/go/bin/go run main.go
0
0
0
0
0
0
(0+0i)
false

Последняя пустая строка это нулевое значение типа 'string'

Можно одновременно описать переменную и инициализировать ее

? cat main.go
package main

import "fmt"

func main() {
  var myInt int = 5
  var myBool bool = true
  var myString string = "vandud"
  fmt.Println(myInt)
  fmt.Println(myBool)
  fmt.Println(myString)
}
? /usr/local/go/bin/go run main.go
5
true
vandud

Второй путь для описания и инициализации переменной - использование короткого оператора описания переменной - :=

? cat main.go
package main

import "fmt"

func main() {
  myVar := "string"
  fmt.Println(myVar)
}
? /usr/local/go/bin/go run main.go
string

В таком случае не требуется использовать ключевое слово var

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

? cat main.go
package main

import "fmt"

func main() {
  var1, var2, var3 := "string", 2, true
  fmt.Println(var1)
  fmt.Println(var2)
  fmt.Println(var3)
}
? /usr/local/go/bin/go run main.go
string
2
true

С предыдущим вариантом тоже можно присвоить значения сразу множеству переменных:

var firstName, lastName string = "Wei-Meng", "Lee"

Если ты описываешь и инициализируешь множество переменных через ключевое слово var - все переменные должны быть одинакового типа
var var4 int, var5 string = 5, "test" - такое на работает но работает если убрать тип данных - var var4, var5 = 5, "test" - как описывалось в начале

Еще переменные можно описать вот так:

? cat main.go
package main

import "fmt"

func main() {
  var (
    name string = "vandud"
    age int = 23
  )
  fmt.Println(name)
  fmt.Println(age)
}
? /usr/local/go/bin/go run main.go
vandud
23

Оператор := работает только внутри функций


Константы это неизменяемые переменные (значение задается один раз на протяжении жизни переменной)
Описываются точно так же как и обычные переменные, но только вместо слова var используется слово const

? cat main.go
package main

import "fmt"

func main() {
  const name string = "vandud"
  fmt.Println(name)
}
? /usr/local/go/bin/go run main.go
vandud

const может использоваться везде где может использоваться var


Объявленные переменные и импортированные пакеты должны использоваться, если они не используются в коде то компилятор выдаст ошибку (потому что неиспользуемая переменная свидетельствует о баге, а неиспользуемые импорты тормозят компиляцию)

Интересный момент: если неиспользуемая переменная объявлена вне функции - то все будет ок

? cat main.go
package main

import "fmt"

var name string = "vandud"

func main() {
  fmt.Println("Hello, World!")
}
? /usr/local/go/bin/go run main.go
Hello, World!

Другими словами: должны быть использованы все переменные объявленные внутри функции

Если все же тебе нужно иметь неиспользуемую переменную, то можно использовать пустой идентификатор _:

? cat main.go
package main

import "fmt"

func main() {
  var test string = "test"
  var _ string = "unused variable"
  fmt.Println(test)
}
? /usr/local/go/bin/go run main.go
test

В Go строки это неизменяемый срез байтов (slice of bytes). Думай о строках как о коллекциях байтов.

Строка может содержать спецсимволы (такие как \n и \t)
Также существуют так называемые raw strings. Они обрамляются обратными тиками ` и могут занимать множество строк. Спецсимволы в "сырых" строках игнорируются

? cat main.go
package main

import "fmt"

func main() {
  var regularString string = "The White House\n1600 Pennsylvania Avenue NW\nWashington, DC 20500\tVandud"
  var rawString string = `The White House\n1600 Pennsylvania Avenue
                          NW\nWashington, DC 20500
                          \tVandud`
  fmt.Println("regularString:")
  fmt.Println(regularString)
  fmt.Println()
  fmt.Println("rawString:")
  fmt.Println(rawString)
}
? /usr/local/go/bin/go run main.go
regularString:
The White House
1600 Pennsylvania Avenue NW
Washington, DC 20500    Vandud

rawString:
The White House\n1600 Pennsylvania Avenue
                          NW\nWashington, DC 20500
                          \tVandud

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

? cat main.go
package main

import "fmt"

func main() {
  var japanese string = "こんにちは世界"
  var japaneseUnicode string = "\u3053\u3093\u306b\u3061\u306f\u4e16\u754c"

  fmt.Println("japanese:")
  fmt.Println(japanese)
  fmt.Println()
  fmt.Println("japaneseUnicode:")
  fmt.Println(japaneseUnicode)
}
? /usr/local/go/bin/go run main.go
japanese:
こんにちは世界

japaneseUnicode:
こんにちは世界

Но если мы заиспользуем функцию len() то увидим число которое больше чем кол-во символов:

? cat main.go
package main

import "fmt"

func main() {
  var japanese string = "こんにちは世界"

  fmt.Println("japanese:")
  fmt.Println(japanese)
  fmt.Println("len(japanese):")
  fmt.Println(len(japanese))
}
? /usr/local/go/bin/go run main.go
japanese:
こんにちは世界
len(japanese):
21

Подробнее почему так:
В Go используется UTF-8
Если смотреть по этим ссылкам, то видно что в UTF-8 каждый символ кодируется одним и тремя байтами соответственно

? cat main.go
package main

import "fmt"

func main() {
  var L string = "\u004C"
  var j string = "\u8ECA"
  fmt.Println(len(L))
  fmt.Println(len(j))
}

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

? /usr/local/go/bin/go run main.go
1
3

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

Если хочешь посчитать не количество байтов в строке, а количество символов (runes), то используй функцию RuneCountInString()

? cat main.go
package main

import "fmt"
import "unicode/utf8"

func main() {
  var japanese = "こんにちは世界"
  fmt.Println(japanese)
  fmt.Println(len(japanese))
  fmt.Println(utf8.RuneCountInString(japanese))
}
? /usr/local/go/bin/go run main.go
こんにちは世界
21
7

Переход типов происходит когда ты хочешь перевести данные из одного типа в другой (например строка с числом превращается в число, это нужно потому что пока число в типе "строка", то ты не сможешь производить с ним какие-либо математические операции)

Так как Go строго типизированный язык - нужно производить все конвертации типов явно. Для этого часто требуется уметь понять какого типа твоя переменная
Например

firstName, lastName, age := "Wei-Meng", "Lee", 25

В выражении выше мы можем легко угадать типы данных переменных: string, string, int
Но какого типа будет переменная start из выражения ниже?

start := time.Now()

Это уже не так очевидно. Для подобных случаев рассмотрим два варианта выяснения типа данных переменной:

  • fmt.Printf("%T\n", start)
  • reflect package
package main

import "fmt"
import "time"
import "reflect"

func main() {
  start := time.Now()
  fmt.Printf("%T\n", start)
  fmt.Println(reflect.TypeOf(start))
}

Работает так

? /usr/local/go/bin/go run main.go
time.Time
time.Time

Очень часто есть необходимость конвертации переменных из одного типа в другой
Рассмотрим пример:

? cat main.go
package main

import "fmt"

func main() {
  var age int
  fmt.Print("Please enter your age: ")
  fmt.Scanf("%d", &age)
  fmt.Println("You entered:", age)
}
? /usr/local/go/bin/go run main.go
Please enter your age: 23
You entered: 23

В примере выше сперва описывается переменная age типа int (соответственно она инициализируется нулевым значением типа int - 0)
Дальше с помощью функции Scanf() мы читаем ввод пользователя с консоли (через спецификатор %d указываем что принимаем на вход десятичное число)
Когда на вход подается число то все работает как и ожидалось
Но если подать на вход слово, то происходит следующее:

? /usr/local/go/bin/go run main.go
Please enter your age: test
You entered: 0
? est
bash: est: command not found
? /usr/local/go/bin/go run main.go
Please enter your age: 23test
You entered: 23
? est
bash: est: command not found

Причина такого поведения кроется в функции Scanf(), которая сперва сканирует текст с stdin, сохраняет подходящие значения (подходящие под спецификатор %d), далее в нашем случае она пытается найти там числа и как только находится подходящее значение - он переходит к следующему выражению кода
Все неподошедшие значения переносятся либо на следующие операторы ввода, либо если программа завершается, то попытаются выполниться как команда оболочки (что видно в последнем примере выше)

Знак & отдает адрес памяти указанной переменной, то есть получается что функция Scanf() читает ввод и подходящие значения кладет в память выделенную для переменной

? cat main.go
package main

import "fmt"

func main() {
  var myVar int
  fmt.Println(&myVar)
}
? /usr/local/go/bin/go run main.go
0xc0000b2008

Чтобы сделать программу лучше, можно сперва считать ввод как строку, а потом с помощью функции Atoi() из пакета strconv попытаться преобразовать ее в число

  • Atoi = ASCII to integer
  • Itoa = Integer to ASCII

Функция Atoi() возвращает два значения: результат конвертации и ошибку (если есть). Чтобы проверить произошла ли ошибка при конвертации надо проверить содержит ли переменная err значение nil

? cat main.go
package main

import "fmt"
import "strconv"

func main() {
  var age int
  var input string

  fmt.Print("Enter your age: ")
  fmt.Scanf("%s", &input)
  age, err := strconv.Atoi(input)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println("Your age is:", age)
  }
}
? /usr/local/go/bin/go run main.go
Enter your age: 23
Your age is: 23

? /usr/local/go/bin/go run main.go
Enter your age: Ivan
strconv.Atoi: parsing "Ivan": invalid syntax

? /usr/local/go/bin/go run main.go
Enter your age: 23Ivan
strconv.Atoi: parsing "23Ivan": invalid syntax

Чтобы сконвертировать строки в какие-то специфические типы данных, например boolean, float, integer, ты можешь использовать различные Parseфункции - https://pkg.go.dev/strconv#ParseBool

? cat main.go
package main

import "fmt"
import "strconv"

func main() {
  var is_gay bool
  var input string

  fmt.Print("Are you gay? [t/f]: ")
  fmt.Scanf("%s", &input)
  is_gay, err := strconv.ParseBool(input)
  if err != nil {
    fmt.Println(err)
  } else if is_gay == true {
    fmt.Println("You are gay!")
  } else {
    fmt.Println("You are not gay(")
  }
}
? /usr/local/go/bin/go run main.go
Are you gay? [t/f]: t
You are gay!

? /usr/local/go/bin/go run main.go
Are you gay? [t/f]: f
You are not gay(

? /usr/local/go/bin/go run main.go
Are you gay? [t/f]: 123
strconv.ParseBool: parsing "123": invalid syntax

Для конвертации численных значений между собой есть встроенные функции, такие как: int(), float32(), float64()
Возможные значения для каждого типа данных описаны тут - https://go.dev/ref/spec#Numeric_types


Достаточно часто необходимо выводить значения нескольких переменных в одной строке

Интерполированная строка — это строковый литерал, который может содержать выражения интерполяции. При разрешении интерполированной строки в результирующую элементы с выражениями интерполяции заменяются строковыми представлениями результатов выражений

Например мы имеем две переменные

queue := 5
name := "John"

И хотим сделать что-то типа

s := name + ", your queue number is:" + queue

У нас ничего не выйдет) Потому что переменная queue типа int, и ее нельзя просто так сконкатенировать со строкой
Нужно конвернтировать int в string и тогда можно будет сконкатенировать их

? cat main.go
package main

import (
    "fmt"
    "strconv"
)

func main() {
  var name string
  var age int

  fmt.Print("Enter your name: ")
  fmt.Scanf("%s", &name)
  fmt.Print("Enter your age: ")
  fmt.Scanf("%d", &age)
  s := "Hi, " + name + "! Your age is " + strconv.Itoa(age)
  fmt.Println(s)
}
? /usr/local/go/bin/go run main.go
Enter your name: Ivan
Enter your age: 23
Hi, Ivan! Your age is 23

Но это решение выглядит слишком громоздким и неудобным, особенно когда у вас большое количество переменных
Лучшим решением будет использовать функцию Sprintf() из пакета fmt

  • Sprintf() вернет строку, а не напечатает ее - s := fmt.Sprintf(format, a)
  • Printf() напечатает отформатированную строку - fmt.Printf(format, a)
? cat main.go
package main

import (
    "fmt"
)

func main() {
  var name string
  var age int

  fmt.Print("Enter your name: ")
  fmt.Scanf("%s", &name)
  fmt.Print("Enter your age: ")
  fmt.Scanf("%d", &age)
  fmt.Printf("Hi, %s! Your age is %d\n", name, age)
}
? /usr/local/go/bin/go run main.go
Enter your name: Ivan
Enter your age: 23
Hi, Ivan! Your age is 23

CHAPTER 3: Making Decisions

Чтобы программа была полезной важно уметь принимать какие-либо решения на основе значений переменных
Go предоставляет для этого три конструкции:

  • if/else
  • switch
  • select

select нужен для каналов (будет рассмотрен позже)


Конструкция if/else простыми словами работает так: Делай X если то-то и то-то истинно, иначе делай Y
Это работает на булевых значениях (true/false)
Этот, казалось бы простой концепт - краеугольный камень программирования

Чтобы получить булево значение обычно используются операторы сравнения приведенные в таблице ниже

Operator Name Example Result
== Equal x == y True if x is equal to y
!= Not equal x != y True if x is not equal to y
< Less than x < y True if x is less than y
<= Less than or equal to x <= y True if x is less than or equal to y
> Greater than x > y True if x is greater than y
>= Greater than or equal to x >= y True if x is greater than or equal to y

Например можем написать проверяльщик на четность
Если введенное число четное - вернет true
Если не четное - вернет false

? cat main.go
package main

import (
    "fmt"
)

func main() {
  var num int
  fmt.Scanf("%d", &num)
  condition := num % 2 == 0
  fmt.Println(condition)
}
? /usr/local/go/bin/go run main.go
5
false

? /usr/local/go/bin/go run main.go
4
true

Можно комбинировать такие логические выражения с помощью логических операторов приведенных в таблице ниже

Operator Name Description Example
&& Logical And Returns true if both statements are true x < y && x > z
Logical Or
! Logical Not Reverse the result, returns false if the result is true !(x == y && x > z)

Например узнаем четно ли введенное число и больше ли оно чем 4

? cat main.go
package main

import (
    "fmt"
)

func main() {
  var num int
  fmt.Scanf("%d", &num)
  condition := (num % 2 == 0) && (num > 4)
  fmt.Println(condition)
}
? /usr/local/go/bin/go run main.go
5
false

? /usr/local/go/bin/go run main.go
6
true

? /usr/local/go/bin/go run main.go
7
false

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

? cat main.go
package main

import (
    "fmt"
)

func main() {
  var num int
  fmt.Scanf("%d", &num)
  condition := (num % 2 == 0) && (num > 4)
  if condition {
    fmt.Println("Number is even and more than 4")
  } else {
    fmt.Println("Number is not even or less than 4")
  }
}
? /usr/local/go/bin/go run main.go
5
Number is not even or less than 4

? /usr/local/go/bin/go run main.go
6
Number is even and more than 4

Логическое выражение можно замунуть прямо в if

...
  if (num % 2 == 0) && (num > 4) {
...

Заворачивать в скобки выражение не требуется, но обязательно нужно заворачивать в фигурные скобки тела конструкции (тот код что идет после слов if и else)

В C например ненулевые значения восприниаются как true, поэтому можно сделать так:

if num % 2 {
  ...
}

В Go так нельзя, будет ошибка non-bool num % 2 (type int) used as if condition


Go выполняет выражение используя метод известный как 'short-circuiting' (короткое замыкание)

Аналогия из книги:
Допустим ты хочешь выбрать выходить ли тебе из дома. До тех пор пока на улице дождь или снег - ты хочешь оставаться дома. Чтобы принять решение ты проверяешь есть ли на улице дождь, если дождь идет то незачем проверять идет ли снег, потому что первое выражение из условия уже выполнилось и ты остаешься дома
И наоборот, ты никогда не видел чтобы шел снег и одновременно шел дождь. Поэтому если идет снег и идет дождь то ты хочешь выйти на улицу чтобы не пропустить это редкое явление. Таким образом если на улице нет дождя, то незачем проверять есть ли там снег, потому что первое выражение условия уже ложно и ты остаешься дома

Пример объясняющий это:
Имеем две функции

func raining() bool {
    fmt.Println("Check if it is raining now...")
    return true
}
func snowing() bool {
    fmt.Println("Check if it is snowing now...")
    return true
}

И условную конструкцию

if raining() || snowing() {
  fmt.Println("Stay indoors!")
}

Получаем

? /usr/local/go/bin/go run main.go
Check if it is raining now...
Stay indoors!

Видим что функция snowing() даже не вызвалась, потому что raining() вернула true и незачем выполнять еще одну функцию (от результат которой ничего не изменится)

Сделаем иначе

if !raining() || snowing() {
  fmt.Println("Let's ski!")
}

И увидим что обе функции вызвались

? /usr/local/go/bin/go run main.go
Check if it is raining now...
Check if it is snowing now...
Let's ski!

Аналогично с and

if !raining() && !snowing() {
    fmt.Println("Let's go outdoors!")
}
? /usr/local/go/bin/go run main.go
Check if it is raining now...

И еще пример

if raining() && snowing() {
    fmt.Println("It's going to be really cold!")
}
? /usr/local/go/bin/go run main.go
Check if it is raining now...
Check if it is snowing now...
It's going to be really cold!

В Go есть удобная вещь
Например у нас есть такая функция

func doSomething() (int, bool) {
  //...
  // just an example of some return values
  return 5, false
}

И мы хотим заиспользовать ее как-то так

v, err := doSomething()
if err {
  // handle the error
} else {
  fmt.Println(v)
}

В таком случае нам нужно знать заранее что переменные v и err у нас нигде не используются

Чтобы обойти эту проблему можно сделать так

if v, err := doSomething(); !err {
  fmt.Println(v)
} else {
  // handle the error
}

Таким образом мы ограничиваем зону видимомости переменных v и err конструкцией if (попытка получить их значения вне if/else приведет к ошибке)


Во многих языках есть тернарный оператор, который принимает три операнда

res = expr ? x: y

Например если ты хочешь сделать что-то такое:

num = 5
parity = num % 2 == 0 ? "even" : "odd"

То придется делать это вот так:

num := 5
parity := ""
if num % 2 == 0 {
  parity = "even"
} else {
  parity = "odd"
}

Потому что в Go нет тернарного оператора
Разработчики языка говорят что так язык чище
https://go.dev/doc/faq#Does_Go_have_a_ternary_form

(в примере выше правильнее будет написать функцию)


Когда у тебя много выражений if/else, код становится сложно читать
Чтобы упростить это можно использовать конструкцию switch
Пример использования

? cat main.go
package main

import (
    "fmt"
)

func main() {
  fmt.Print("Enter a number of weekday: ")
  var num int
  fmt.Scanf("%d", &num)
  dayOfWeek := ""
  switch num {
    case 1:
      dayOfWeek = "Monday"
    case 2:
      dayOfWeek = "Tuesday"
    case 3:
      dayOfWeek = "Wednesday"
    case 4:
      dayOfWeek = "Thursday"
    case 5:
      dayOfWeek = "Friday"
    case 6:
      dayOfWeek = "Saturday"
    case 7:
      dayOfWeek = "Sunday"
    default:
      dayOfWeek = "--error--"
  }
  fmt.Println(dayOfWeek) // Friday
}
? /usr/local/go/bin/go run main.go
Enter a number of weekday: 1
Monday

? /usr/local/go/bin/go run main.go
Enter a number of weekday: 7
Sunday

Эта конструкция сравнивает переменную num с предоставленными вариантами и как только находит совпадение - выполняет блок инструкций
Если ни одно не подошло - выполняет default
После выполнения выбранных инструкций управление передается из конструкции switch (выход из switch)
(то есть если мы введем "1" то выполнится кейс 1 и остальные кейсы просто проигнорируются)

В отличие от языка C, тут не надо использовать оператор break на конце каждого кейса

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

switch num {
    case 1:
        dayOfWeek = "Monday"
        fmt.Println("Monday blues...")
    case 2: dayOfWeek = "Tuesday"
...

По умолчанию когда выполнился код из кейса, то управление передает во вне switch
Иногда хочется избежать такого поведения
Для этого нужно использовать ключевое слово fallthrough
Оно передает управление не во вне, а телу следующего кейса

? cat main.go
package main

import "fmt"

func main() {
  var grade string
  fmt.Scan(&grade)
  switch grade {
    case "A":
      fallthrough
    case "B":
      fallthrough
    case "C":
      fallthrough
    case "D":
      fmt.Println("Passed")
    case "F":
      fmt.Println("Failed")
    default:
      fmt.Println("Absent")
  }
}
? /usr/local/go/bin/go run main.go
A
Passed

? /usr/local/go/bin/go run main.go
E
Absent

? /usr/local/go/bin/go run main.go
F
Failed

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

switch grade {
    case "A", "B", "C", "D":
        fmt.Println("Passed")
    case "F":
        fmt.Println("Failed")
    default:
        fmt.Println("Undefined")
}

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

Еще можно сделать вот так чтобы избежать больших if/else конструкций

score := 79
grade := ""
switch {
    case score < 50: grade = "F"
    case score < 60: grade = "D"
    case score < 70: grade = "C"
    case score < 80: grade = "B"
    default: grade = "A"
}
fmt.Println(grade)    // B

CHAPTER 4: Over and Over and Over: Using Loops

В отличие от таких языков как C и Java, Go имеет только один вариант цикла - for (в упомянутых языках есть еще while,do и while)
Выглядит так:

for (init; condition; post) {
}

Включает три компоненты:

  • init - выполняется перед первой итерацией
  • condition - выполняется перед итерацией чтобы понять должна ли итерация выполниться
  • post - выполняется в конце каждой итерации

condition - должен быть булевым значением, init и post просто выражения для выполнения

Преобразуем базовый цикл:

for i:=0; i<5; i++ {
  fmt.Println(i)
}

Во что-то такое:

? cat main.go
package main

import "fmt"

func condition(i int) bool {
  fmt.Println("cond:", i)
  if i == 3 {
    return false
  } else {
    return true
  }
}
func myinit() int {
  fmt.Println("-- init --")
  return 0
}
func post(i int) int {
  fmt.Println("post:", i)
  return i + 1
}

func main() {
  for i := myinit(); condition(i); i = post(i) {
    fmt.Println("body:", i)
  }
}
? /usr/local/go/bin/go run main.go
-- init --
cond: 0
body: 0
post: 0
cond: 1
body: 1
post: 1
cond: 2
body: 2
post: 2
cond: 3

Видим что:

  • init - выполнился один раз в самом начале (инициализация счетчика)
  • далее выполнение идет в порядке: cond > body > post, до тех пор пока на месте cond не окажется логичский false

Screenshot_2021_02_02-12_49_03-2022-12-12-at-20stnh.png

Другими словами

В Go функция не может иметь имя init


В таких языках как C и Java есть операторы pre и post-increment/decrement

operator description
num++ post-increment operator
++num pre-increment operator
num-- post-decrement operator
--num pre-decrement operator

В Go нет концепции pre и post, есть просто ++ и -- операторы которые увеличивают или уменьшают значение переменной

Также важно понимать что ++ это оператор, а не выражение (он ничего не возвращает)
Поэтому fmt.Println(i++) приведет к ошибке

Компоненты цикла (init, condition, post) не нужно оборачивать в скобки:

? cat main.go
package main

import "fmt"

func main() {
    for (i := 0; i < 1; i++) { fmt.Println(i) }
}
? /usr/local/go/bin/go run main.go
# command-line-arguments
./main.go:6:12: syntax error: unexpected :=, expecting )

К тому же секции init и post в цикле - опциональны:

? cat main.go
package main

import "fmt"

func main() {
    max := 13
    a, b := 0, 1
    for ; b <= max; {
        fmt.Println(b)
        a, b = b, a+b
    }
}
? /usr/local/go/bin/go run main.go
1
1
2
3
5
8
13

Очевидно как можно сделать бесконечный цикл) Точно также и while легко заменяется for'ом

Точки с запятыми также не обязательны, описанный выше цикл можно написать и без них:

for b <= max {
    println(b)
a, b = b, a+b }

for без всех трех компонент - бесконечный for

i := 0
for {
  fmt.Println(i)
  i++
}

Чтобы выйти из цикла можно использовать слово break:

? cat main.go
package main

import "fmt"

func main() {
    i := 0
    for {
        if i == 3 { break }
        fmt.Println(i)
        i++
    }
}
? /usr/local/go/bin/go run main.go
0
1
2

Кроме break есть еще continue, оно позволяет скипнуть остаток цикла и перейти к новой итерации:

? cat main.go
package main

import "fmt"

func main() {
    i := 0
    for {
        i++
        if i == 7 { break }
        if i % 2 != 0 { continue }
        fmt.Println(i)
    }
}
? /usr/local/go/bin/go run main.go
2
4
6

Интересно что всю генерацию последовательности Фибоначчи можно уместить в один цикл:

? cat main.go
package main

func main() {
  for max, a, b := 21, 0, 1; b <= max; a, b = b, a + b {
    println(b)
  }
}
? /usr/local/go/bin/go run main.go
1
1
2
3
5
8
13
21

Часто необходимо итерироваться в цикле по какому-то набору значений (массивы, строки, итд)
Базово массивы и слайсы это коллекции айтемов
Например у нас есть массив из трех эллементов:

var OS [3]string
OS[0] = "iOS"
OS[1] = "Android"
OS[2] = "Windows"

Чтобы проитерироваться через каждый из элементов можно использовать for-range цикл

? cat main.go
package main

func main() {

    var OS [3]string
    OS[0] = "iOS"
    OS[1] = "Android"
    OS[2] = "Windows"

    for i, v := range OS { println(i, "-", v) }

}
? /usr/local/go/bin/go run main.go
0 - iOS
1 - Android
2 - Windows

Ключевое слово range возвращает два значения:

  • index
  • value

Если какое-то из значений не нужно (индекс или значение), то его можно передать в пустой идентификатор _:

for _, v := range OS { println(v) }

Потому что помним что неиспользуемые переменные - недопустимы

Пустой идентификатор можно опустить, тогда получим только индексы:

for myvar := range OS { println(myvar) }
 /usr/local/go/bin/go run main.go
0
1
2

Так как строки это слайс байтов, то по ним тоже можно итерироваться (абсолютно аналогично)

? cat main.go
package main

import "fmt"

func main() {
    var input string
    fmt.Scan(&input)
    for pos, char := range input { fmt.Println("pos:", pos, "char:", char) }
}
? /usr/local/go/bin/go run main.go
Ivan
pos: 0 char: 73
pos: 1 char: 118
pos: 2 char: 97
pos: 3 char: 110

Вывод может удивить, символы отобразились не как символы, а как их Unicode'ное числовое представление
Это легко исправить форматированием:

? cat main.go
package main

import "fmt"

func main() {
    var input string
    fmt.Scan(&input)
    for pos, char := range input {
        fmt.Printf("pos: %d char: %c unicode: %d\n", pos, char, char)
    }
}
? /usr/local/go/bin/go run main.go
Ivan
pos: 0 char: I unicode: 73
pos: 1 char: v unicode: 118
pos: 2 char: a unicode: 97
pos: 3 char: n unicode: 110

Если наша строка содержит символы которые занимают больше одного байта (например японские символы), то индекс отображает положение начального байта символа в строке:

? /usr/local/go/bin/go run main.go
私は非常にクールです
pos: 0 char: 私 unicode: 31169
pos: 3 char: は unicode: 12399
pos: 6 char: 非 unicode: 38750
pos: 9 char: 常 unicode: 24120
pos: 12 char: に unicode: 12395
pos: 15 char: ク unicode: 12463
pos: 18 char: ー unicode: 12540
pos: 21 char: ル unicode: 12523
pos: 24 char: で unicode: 12391
pos: 27 char: す unicode: 12377

Ключевые слова continue и break влияют на текущий цикл, а что если мы находимся во вложенном цикле и хотим повлиять на оба?
Например код выводящий таблицу умножения:

? cat main.go
package main

import "fmt"

func main() {
    for i := 1; i <= 5; i++ {
        for j := 1; j <= 5; j++ {
            fmt.Printf("%d * %d = %d\n", i, j, i * j)
        }
        fmt.Println("---------")
    }
}
? /usr/local/go/bin/go run main.go
1 * 1 = 1
1 * 2 = 2
1 * 3 = 3
1 * 4 = 4
1 * 5 = 5
---------
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8
2 * 5 = 10
---------
3 * 1 = 3
3 * 2 = 6
3 * 3 = 9
3 * 4 = 12
3 * 5 = 15
---------
4 * 1 = 4
4 * 2 = 8
4 * 3 = 12
4 * 4 = 16
4 * 5 = 20
---------
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
5 * 4 = 20
5 * 5 = 25
---------

Если мы вставим перед fmt.Printf что-то такое:

if i == 3 {
            break
}

То пропустим только блок таблицы умножения на 3 (тот что по середине)
А мы хотим чтобы не отображались и все остальные, то есть хотим сделать break внешнему циклу

Для решения этой проблемы на внешний цикл можно повесить лейбл и сделать break по лейблу
Выглядит это так:

? cat main.go
package main

import "fmt"

func main() {
    Outerloop:
        for i := 1; i <= 5; i++ {
            for j := 1; j <= 5; j++ {
                if i == 3 { break Outerloop }
                fmt.Printf("%d * %d = %d\n", i, j, i * j)
            }
            fmt.Println("---------")
        }
}
? /usr/local/go/bin/go run main.go
1 * 1 = 1
1 * 2 = 2
1 * 3 = 3
1 * 4 = 4
1 * 5 = 5
---------
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8
2 * 5 = 10
---------

С continue аналогично

CHAPTER 5: Grouping Code into Functions

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

Чтобы вызвать функцию нужно просто вызвать ее по имени за которым следуют скобки

? cat main.go
package main

import "fmt"
import "time"

func displayDate() {
    fmt.Println(time.Now().Date())
}

func main() {
    displayDate()
}
? /usr/local/go/bin/go run main.go
2022 December 14

Фукнция может принимать в себя аргументы, которые будут попадать в нее при вызове

Иногда можно встретить слова "аргумент" и "параметр" которые используются взаимозаменяемо. На самом деле это не одно и то же. Параметр это переменная указанная при описании функции, а аргумент это значение переданное в функцию

Screenshot_2021_02_02-12_49_03-2022-12-14-at-02argparam.png

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

Year: "2006" "06"
Month: "Jan" "January" "01" "1"
Day of the week: "Mon" "Monday"
Day of the month: "2" "_2" "02"
Day of the year: "__2" "002"
Hour: "15" "3" "03" (PM or AM)
Minute: "4" "04"
Second: "5" "05"
AM/PM mark: "PM"

Например если мы укажем в строку форматирования даты другие числа и слова, то получим например такое:

displayDate("Fri 2001-01-02 15:04:05")
Fri 14012-12-14 02:35:58
  • Fri - не распарсился и вывелся как есть
  • 2001 - распарсился как 2(day), 0(as is), 01(month)
  • остальное распарсилось как надо

Более вербозный пример на картинке ниже
Screenshot_2021_02_02-12_49_03-2022-12-14-at-02datetimeformat.png


В отличие от таких языков как C# и Java, в Go не поддерживается function overloading - это функция языка позволяющая иметь множество функций с одним и тем же именем, но с разными сигнатурами (параметрами)
Этой функции нет потому что фундаментальная философия Go - оставить язык простым


Рассмотрим функцию swap()

? cat main.go
package main

import "fmt"

func swap(a, b int) {
    a, b = b, a
}

func main() {
    x, y := 5, 6
    swap(x, y)
    fmt.Println(x, y)
}
? /usr/local/go/bin/go run main.go
5 6

Ожидалось что переменные x, y обменяются значениями, но этого не произошло
Так вышло потому что переданные в функцию значения аргументов скопировались в переменные a, b

Таким образом, чтобы не случилось внутри функции swap(), это не затронет значения переменных x, y из функции main()
Это называется passing by value

В случае если ты хочешь чтобы функция swap() аффектила переменные из main(), то нужно изменить параметры функции так чтобы они принимали указатель на вместо значения

cat main.go
package main

import "fmt"

func swap(a, b *int) {
    *a, *b = *b, *a
}

func main() {
    x, y := 5, 6
    swap(&x, &y)
    fmt.Println(x, y)
}
? /usr/local/go/bin/go run main.go
6 5

В примере выше мы указываем что функция принимает в качестве аргументов указатели (с помощью *) и передаем в нее указатели (с помощью &)
Это passing by pointer aka passing by reference
Теперь функция берет значение из области памяти на которую указывает b и кладет это значение в область памяти на которую указывает a (плюс то же самое но наоборот)


Функция может возвращать значения
Для этого нужно указывать тип возвращаемых данных после скобок с параметрами функции

? cat main.go
package main

import "fmt"

func sum(a, b int) int{
    return a + b
}

func main() {
    x, y := 5, 6
    fmt.Println(sum(x, y))
}
? /usr/local/go/bin/go run main.go
11

Функция может возвращать множество значений

? cat main.go
package main

import "fmt"

func swap(a, b int) (int, int) {
    return b, a
}

func main() {
    x, y := 5, 6
    x, y = swap(x, y)
    fmt.Println(x, y)
}
? /usr/local/go/bin/go run main.go
6 5

В таком случае нужно завернуть несколько типов возвращаемых данных в скобки, как видно выше

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


Также возможно указывать не просто типы возвращаемых значений, а имя возвращаемой переменной:

func sum(a, b int) (sum int) {
    sum = a + b
    return
}

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

В return в таком случае не нужно передавать ничего (хотя можно), потому что возвращаемые значения мы уже описали


Вариативные функции принимают нефиксированное количество аргументов
Одна из самых общих таких функций это fmt.Println()

fmt.Println("Hello")
fmt.Println("Hello", "World!")
fmt.Println("Hello", 123, true)

Чтобы описать такую функцию нужно в параметрах использовать многоточие ...

? cat main.go
package main

import "fmt"

func sum(nums ... int) (sum int) {
    for _, n := range nums {
        sum += n
    }
    return
}

func main() {
    fmt.Println(sum(1, 2, 3, 4, 5))
}
? /usr/local/go/bin/go run main.go
15

Теперь nums это слайс int

Вместе с многоточием можно использовать и обычные параметры

? cat main.go
package main

import "fmt"

func sum(sum int, nums ... int) int {
    for _, n := range nums {
        sum += n
    }
    return sum
}

func main() {
    fmt.Println(sum(5, 1, 2, 3, 4, 5))
}
? /usr/local/go/bin/go run main.go
20

Но как мы видим, относительно предыдущей реализации у нас теперь не именованное возвращаемое значение и return содержит явное указание что возвращать
Это из-за первого параметра функции

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

func sum(nums ... int, sum int) int {
    for _, n := range nums {
        sum += n
    }
    return sum
}

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

? cat main.go
package main

import "fmt"

func main() {
    count := func(input ... int) (sum int) {
        sum = 0
        for _, _ = range input {
            sum++
        }
        return
    }
    fmt.Println(count(1, 2, 3))
    fmt.Println(count(1, 2, 3, 4, 5))
}
? /usr/local/go/bin/go run main.go
3
5

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

Фактически переменная переменная с анонимной функцией определяется так:

var count func(int, int) int = func(x, y int) int{ return x + y}

Анонимная функция может формировать замыкание - closure - значение фукнции которое ссылается на переменные вне функции

func fib() func() int {
    f1 := 0
    f2 := 1
    return func() int {
        f1, f2 = f2, (f1 + f2)
        return f1
    }
}

Функция fib() возвращает функцию которая возвращает int
Другими словами fib() возвращает замыкание

Анонимную функцию делает замыканием то что она имеет доступ к переменным вне себя

Получаем следующее:

? cat main.go
package main

import "fmt"

func fib() func() int {
    f1 := 0
    f2 := 1
    return func() int {
        f1, f2 = f2, (f1 + f2)
        return f1
    }
}
func main() {
    gen := fib()
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
    fmt.Println(gen())
}
? /usr/local/go/bin/go run main.go
1
1
2
3
5

Прелесть этого всего в том что нам не нужно хранить все сгенерированные числа фибоначи, мы храним только последние два


Многие языки которые поддерживают замыкания идут с предопределенными функциями filter(), map(), reduce()

  • filter() - принимает коллекцию элементов и возвращает коллекцию из нужных тебе элементов
  • map() - позволяет замапить элементы одной коллекции в другую
  • reduce() - возвращает одно значение на основе переданной коллекции

К сожалению в Go таких функций из коробки нет
Но их можно имплементировать самому:

? cat main.go
package main

import "fmt"

func filter(arr []int, cond func(int) bool) []int {
    result := []int{}
    for _, v := range arr {
        if cond(v) {
            result = append(result, v)
        }
    }
    return result
}

func main() {
    a := []int{1, 2, 3, 4, 5}
    evens := filter(a,
        func(val int) bool {
            return val%2 == 0
        })
    fmt.Println(evens)
}
? /usr/local/go/bin/go run main.go
[2 4]

В примере выше мы описали функцию filter(), которая принимает в себя слайс интов и возвращает слайс интов и функцию на основе которой будет производиться фильтрация, которая принимает в себя один параметр int и возвращает булево значение

Чтобы вернуть значения больше 3 нужно просто изменить анонимную функцию cond

func main() {
    a := []int{1, 2, 3, 4, 5}
    evens := filter(a,
        func(val int) bool {
            return val > 3
        })
    fmt.Println(evens)
}
No Comments
Back to top