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
Другими словами
В 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
Фукнция может принимать в себя аргументы, которые будут попадать в нее при вызове
Иногда можно встретить слова "аргумент" и "параметр" которые используются взаимозаменяемо. На самом деле это не одно и то же. Параметр это переменная указанная при описании функции, а аргумент это значение переданное в функцию
Кстати время форматируется интересным образом
Парсятся из шаблона только определенные слова
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) - остальное распарсилось как надо
Более вербозный пример на картинке ниже
В отличие от таких языков как 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