Golang: Go in Practice — заметки на полях, часть 1 — введение

Автор: | 02/04/2019
 

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

Выполняем:

go run types.go
int

Второй способ — с помощью модуля 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))
}

Результат:

go run types.go
Using Printf: int
Using Reflect: int

Возврат нескольких значений

В 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)
}

Запускаем:

go run return.go
Foo Bar
Foo

Тут в объявлении функции 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)
}

Результат:

go run return.go
Foo Bar
Foo

Стандартная библиотека

Список всех доступных пакетов доступен на странице 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:

docker run -ti -p 80:80 nginx

Вызываем:

go run net_get.go
HTTP/1.1 200 OK

И проверяем лог 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()
}

Запускаем:

go run http_get.go
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Тут выполняется:

  1. вызываем функцию http.Get(), которой передаём адрес запроса
  2. вызываем ioutil.ReadAll(), которой передаём ответ, полученный из http.Get()
  3. выводим полученный результат на консоль, форматируя его в строку
  4. закрываем подключение с помощью 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)
}

И результат её выполнения:

go run mthread.go
1
Hello, world
2
3
4

В функции 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():

  1. создаём канал — c := make(chan int)
  2. вызываем функцию printCount(), которой передаём канал
  3. в цикле for передаём в канал элементы массива а

И результат выполнения:

go run channel.go
0 1 2 3 4 5 6 End of Main

Управление пакетами в 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:

go env
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/setevoy/.cache/go-build"
GOEXE=""
GOFLAGS=""
...

Или только определённую:

go env GOROOT
/usr/lib/go

В $GOROOT так же содержатся каталоги bin и src:

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

В $GOPATH будут содержаться внешние библиотеки и ваш код, и по этому пути Go будет их искать, если они используются в import.

Можно задавать несколько путей, разделив их :, как с обычным PATH в Linux.

Кроме того — $GOPATH имеет смысл добавить в PATH, что бы вызывать собранные и ипортированные пакеты без указания полного пути.

Для этого в .bashrc добавляем:

...
export PATH=$PATH:$GOPATH/bin
...

Создадим каталог в текущей директории для пакетов:

mkdir $(pwd)/packages

Задаём GOPATH:

export GOPATH=$(pwd)/packages

Проверяем значение:

go env GOPATH
/home/setevoy/Scripts/Go/packages

Загружаем пакет net/html:

go get golang.org/x/net/html

Проверяем содержимое:

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
...

Аналогично можно выполнить импорт пакета из Git или SVN репозиториев.

Тестирование

Go также включает в себя встроенные инструменты для выполнения юнит-тестов.

go test выполнит поиск файлов, заканчивающихся на _test, а затем проверит выполнение всех модулей в текущем каталоге.

Создаём тестовый каталог:

mkdir test
cd test/

Создаём файл 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, и создаём функцию Testnamego test выполнит функции, начинающиеся на Test:

package main

import "testing"

func TestName(t *testing.T) {
    name := getName()
    if name != "World" {
        t.Error("Wrong return value -", name)
    }
}

Проверяем:

go test
PASS
ok      _/home/setevoy/Scripts/Go/test  0.001s

Поменяем «World» на что-то другое:

func getName() string {
    return "Bar"
}

И тест вернёт ошибку:

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

HTTP-сервер на Go

Создадим простое приложение, которое будет использовать пакет net/http, и будет выводить в браузере текст.

Создаём каталог:

mkdir hello_go

Создаём файл 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:

go run hello.go

Либо выполнить компиляцию в исполняемый файл:

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

И запустить hello_go:

./hello_go

Проверяем в браузере:

curl localhost:8080
Hello, world

Готово.