Ещё весной начал учить Go, но потом перешёл на новую работу, забот и без того хватало, и Go забросил.
Тем не менее — встречается он сейчас много где (Docker, Prometheus, Terraform etc), а потому знать его желательно.
Да и вообще — иногда надо поучить что-то новое, ибо становится скучно.
Попробую вести «конспекты» книги Go in Practice авторов Matt Butcher, Matt Farina — на мой взгляд одна из самых толковых книг по Go (при условии, что это не первый ваш язык программирования, конечно). Не факт, что писать буду много и долго, так как сам Go мне совсем не нравится, но графомании ради — пусть будет.
Тут будет именно конспектирование, так что чтение самой книги категорически рекомендуется.
Кроме этой книги — ещё можно обратить внимание на The Go Programming Language, авторов Alan A. A. Donovan и Brian W. Kernighan (того самого, который соавтор книги C Programming Language).
Также есть серия переводов Go с нуля, не закончена — но базовые вещи рассмотрены. Остальные можно почитать в оригинале.
Ещёможно пройти быстрый туториал на tour.golang.org.
Содержание
Объявление переменных
Вспомним объявление переменных в Go.
Можно использовать полное объявление:
var i int = 1
В случае, если переменной сразу присваивается значение — можно опустить указание типа, тогда Go сам определит её тип:
var i = 1
Объявление переменной с помощью var тоже можно пропустить, и использовать объявление переменной с помощью «:» и оператора «=«:
i := 1
Вывод типа переменной
Примеры того, как можно отобразить типа переменных в Golang.
Первый способ — с помощью модификатора %T в Printf():
package main
import "fmt"
func main() {
i := 1
fmt.Printf("%T\n", i)
}
Выполняем:
[simterm]
$ go run types.go int
[/simterm]
Второй способ — с помощью модуля reflect:
package main
import "fmt"
import "reflect"
func main() {
i := 1
fmt.Printf("Using Printf: %T\n", i)
fmt.Printf("Using Reflect: %s\n", reflect.TypeOf(i))
}
Результат:
[simterm]
$ go run types.go Using Printf: int Using Reflect: int
[/simterm]
Возврат нескольких значений
В Go имеется встроенная поддержка возврата нескольких значений из функций.
Пример:
package main
import "fmt"
func Names() (string, string) {
return "Foo", "Bar"
}
func main() {
// assing values returned bu Names() to n1, n2 vars
n1, n2 := Names()
fmt.Println(n1, n2)
// assing only first value from Names() to n3, drop second
n3, _ := Names()
fmt.Println(n3)
}
Запускаем:
[simterm]
$ go run return.go Foo Bar Foo
[/simterm]
Тут в объявлении функции Name() указываем типы возвращаемых ей значений — func Names() (string, string).
Кроме того — в Name() можно задать имена переменных, которые она будет возвращать:
package main
import "fmt"
func Names() (a string, b string) {
a = "Foo"
b = "Bar"
return
}
func main() {
// assing values returned bu Names() to n1, n2 vars
n1, n2 := Names()
fmt.Println(n1, n2)
// assing only first value from Names() to n3, drop second
n3, _ := Names()
fmt.Println(n3)
}
Результат:
[simterm]
$ go run return.go Foo Bar Foo
[/simterm]
Стандартная библиотека
Список всех доступных пакетов доступен на странице https://golang.org/pkg/.
Пакет net
Dial()
Для работы с сетью можно использовать пакет net.
Создадим подключение, выполним GET и выведем ответ:
package main
import (
"bufio"
"fmt"
"net"
)
func main() {
conn, _ := net.Dial("tcp", ":80")
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, _ := bufio.NewReader(conn).ReadString('\n')
fmt.Println(status)
}
Запускаем NGINX:
[simterm]
$ docker run -ti -p 80:80 nginx
[/simterm]
Вызываем:
[simterm]
$ go run net_get.go HTTP/1.1 200 OK
[/simterm]
И проверяем лог NGINX:
172.17.0.1 — — [08/Nov/2018:10:08:06 +0000] «GET / HTTP/1.0» 200 612 «-» «-» «-«
Тут в conn, _ := net.Dial("tcp", ":80") — вызываем функцию Dial() из пакета net, которой передаём тип подключения tcp, и адрес в виде :порт.
Если в паре адрес:порт адрес не указан — по умолчанию используется localhost.
Затем с помощью fmt.Fprintf пишем в conn текст запроса, и далее с помощью ReadString — читаем полученный ответ, заносим его в status, и с помощью fmt.Println() — выводим его на консоль.
Аналогично, если не указать порт — то будет использоваться 80.
http
В стандартной библиотеке так же имеется пакет http.
Пример выполенния GET с его использованием:
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main () {
resp, _ := http.Get("http://localhost")
body, _ := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
resp.Body.Close()
}
Запускаем:
[simterm]
$ go run http_get.go <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
[/simterm]
Тут выполняется:
- вызываем функцию
http.Get(), которой передаём адрес запроса - вызываем
ioutil.ReadAll(), которой передаём ответ, полученный изhttp.Get() - выводим полученный результат на консоль, форматируя его в строку
- закрываем подключение с помощью
resp.Body.Close()
Мультипоточность и каналы
Go изначально разрабатывался с учётом работы на многопроцессорных системах и поддерживает многопоточность.
Пример выполнения функции count() одновременно с основной main():
package main
import (
"fmt"
"time"
)
func count() {
for i := 0; i < 5; i++ {
fmt.Println(i)
time.Sleep(time.Millisecond * 1)
}
}
func main () {
// run in thread
go count()
time.Sleep(time.Millisecond * 2)
fmt.Println("Hello, world")
time.Sleep(time.Millisecond * 5)
}
И результат её выполнения:
[simterm]
$ go run mthread.go 0 1 Hello, world 2 3 4
[/simterm]
В функции main() выполняем вызов count(), и обе функции выполняются одновременно — count() выводит числа, main() выводит «Hello, World«.
Каналы
Использование каналов может использоваться для передачи данных между потоками выполнения из функции в функцию, и позволяет выполнять синхронизацию их выполнения.
По ходу дела нагуглил интересные примеры тут>>>.
Пример использования канала:
package main
import (
"fmt"
)
func printCount(c chan int) {
num := 0
for num >= 0 {
num = <-c
fmt.Print(num," ")
}
}
func main() {
// create channel
c := make(chan int)
// create array
a := []int{0, 1, 2, 3, 4, 5, 6}
// run in thread with channel passed
go printCount(c)
// pass items from a in loop to this channel
for _, v := range a {
c <- v
}
fmt.Println("End of Main")
}
Тут в main():
- создаём канал —
c := make(chan int) - вызываем функцию
printCount(), которой передаём канал - в цикле
forпередаём в канал элементы массива а
И результат выполнения:
[simterm]
$ go run channel.go 0 1 2 3 4 5 6 End of Main
[/simterm]
Управление пакетами в Go
В примерах выше уже использовалось управление пакетами — import, например пакет fmt импортируется как:
import "fmt"
Или:
import (
"fmt"
)
После чего в коде его можно использовать по имени fmt, вызывая функции, включенные в этот пакет:
package main
import "fmt"
func main() {
fmt.Println("Hello, World")
}
Второй вариант используется, если импортируется несколько пакетов:
import (
"fmt"
"net/http"
)
Кроме того, менеджер пакетов Go позволяет таким же образом импортировать пакеты, не входящие в стандартную библиотеку, например:
import (
"golang.org/x/net/html"
"fmt"
"net/http"
)
GOPATH
Что бы выполнить импорт внешнего пакета — необходимо задать $GOPATH, и затем вызвать go get.
Проверить все текущие переменные Go можно с помощью go env:
[simterm]
$ go env GOARCH="amd64" GOBIN="" GOCACHE="/home/setevoy/.cache/go-build" GOEXE="" GOFLAGS="" ...
[/simterm]
Или только определённую:
[simterm]
$ go env GOROOT /usr/lib/go
[/simterm]
В $GOROOT так же содержатся каталоги bin и src:
[simterm]
$ ll /usr/lib/go total 28 drwxr-xr-x 2 root root 4096 Nov 9 10:17 api drwxr-xr-x 2 root root 4096 Nov 9 10:17 bin lrwxrwxrwx 1 root root 17 Nov 5 11:52 doc -> /usr/share/doc/go drwxr-xr-x 3 root root 4096 May 23 13:27 lib drwxr-xr-x 14 root root 4096 Nov 9 10:17 misc drwxr-xr-x 10 root root 4096 May 23 13:27 pkg drwxr-xr-x 46 root root 4096 Nov 9 10:17 src -rw-r--r-- 1 root root 8 Nov 5 11:52 VERSION
[/simterm]
В $GOPATH будут содержаться внешние библиотеки и ваш код, и по этому пути Go будет их искать, если они используются в import.
Можно задавать несколько путей, разделив их :, как с обычным PATH в Linux.
Кроме того — $GOPATH имеет смысл добавить в PATH, что бы вызывать собранные и ипортированные пакеты без указания полного пути.
Для этого в .bashrc добавляем:
... export PATH=$PATH:$GOPATH/bin ...
Создадим каталог в текущей директории для пакетов:
[simterm]
$ mkdir $(pwd)/packages
[/simterm]
Задаём GOPATH:
[simterm]
$ export GOPATH=$(pwd)/packages
[/simterm]
Проверяем значение:
[simterm]
$ go env GOPATH /home/setevoy/Scripts/Go/packages
[/simterm]
Загружаем пакет net/html:
[simterm]
$ go get golang.org/x/net/html
[/simterm]
Проверяем содержимое:
[simterm]
$ tree -d packages/
packages/
├── pkg
│ └── linux_amd64
│ └── golang.org
│ └── x
│ └── net
└── src
└── golang.org
└── x
└── net
├── bpf
│ └── testdata
├── context
│ └── ctxhttp
├── dict
├── dns
│ └── dnsmessage
├── html
│ ├── atom
│ ├── charset
│ │ └── testdata
│ └── testdata
│ ├── go
│ └── webkit
│ └── scripted
...
[/simterm]
Аналогично можно выполнить импорт пакета из Git или SVN репозиториев.
Тестирование
Go также включает в себя встроенные инструменты для выполнения юнит-тестов.
go test выполнит поиск файлов, заканчивающихся на _test, а затем проверит выполнение всех модулей в текущем каталоге.
Создаём тестовый каталог:
[simterm]
$ mkdir test $ cd test/
[/simterm]
Создаём файл hello.go, в котором функция getName() должна вернуть значение «World«:
package main
import "fmt"
func getName() string {
return "World"
}
func main() {
name := getName()
fmt.Println("Hello ", name)
}
И файл hello_test.go, в котором используем модуль testing, и создаём функцию Testname — go test выполнит функции, начинающиеся на Test:
package main
import "testing"
func TestName(t *testing.T) {
name := getName()
if name != "World" {
t.Error("Wrong return value -", name)
}
}
Проверяем:
[simterm]
$ go test PASS ok _/home/setevoy/Scripts/Go/test 0.001s
[/simterm]
Поменяем «World» на что-то другое:
func getName() string {
return "Bar"
}
И тест вернёт ошибку:
[simterm]
$ go test
--- FAIL: TestName (0.00s)
hello_test.go:8: Wrong return value - Bar
FAIL
exit status 1
FAIL _/home/setevoy/Scripts/Go/test 0.002s
[/simterm]
HTTP-сервер на Go
Создадим простое приложение, которое будет использовать пакет net/http, и будет выводить в браузере текст.
Создаём каталог:
[simterm]
$ mkdir hello_go
[/simterm]
Создаём файл hello.go:
package main
import (
"fmt"
"net/http"
)
func hello(res http.ResponseWriter, req *http.Request) {
fmt.Fprint(res, "Hello, world")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe("localhost:8080", nil)
}
Тут в функции http.HandleFunc("/", hello) вызываем обработчик пути — при обращении к URI «/» он будет вызывать функцию hello(), а в строке http.ListenAndServe() — запускаем веб-сервер, который будет слушать localhost на порту 8080.
Теперь можно его запустить либо без сборки с помощью go run:
[simterm]
$ go run hello.go
[/simterm]
Либо выполнить компиляцию в исполняемый файл:
[simterm]
$ go build $ ll total 6408 -rw-r--r-- 1 setevoy setevoy 227 Nov 13 17:11 hello.go -rwxr-xr-x 1 setevoy setevoy 6554305 Nov 13 17:13 hello_go
[/simterm]
И запустить hello_go:
[simterm]
$ ./hello_go
[/simterm]
Проверяем в браузере:
[simterm]
$ curl localhost:8080 Hello, world
Готово.





