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

Автор: | 04/02/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)
}

Выполняем:

[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]

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

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

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

[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():

  1. создаём канал – c := make(chan int)
  2. вызываем функцию printCount(), которой передаём канал
  3. в цикле 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, и создаём функцию Testnamego 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

[/simterm]

Готово.