Ещё весной начал учить 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
Готово.