🇺🇦 Go for two :)
1.18K subscribers
22 photos
3 files
184 links
Telegram channel about tricks and engineering practices in the Go programming language over a cup of coffee ☕️.

author: @a_soldatenko
personal blog: https://asoldatenko.org

#golang #go #kubernetes #debugging
加入频道
Note #4 Дебажим приложение на Go в докере 🐳

Итак нам понадобится:
- прямые руки и тазик с предустановленным докером

$ cat Dockerfile
FROM golang:1.13

WORKDIR /go/src/app
COPY . .

RUN go get -u github.com/go-delve/delve/cmd/dlv

CMD ["app"]


$ docker build -t my-golang-app .

# Это всего лишь один из вариантов, иногда нужно вместо bash сразу dlv запускать и так далее
$ docker run -it --rm my-golang-app bash

$ root@03c1977b1063:/go/src/app# dlv main.go
Error: unknown command "main.go" for "dlv"
Run 'dlv --help' for usage.
root@03c1977b1063:/go/src/app# dlv debug main.go
could not launch process: fork/exec /go/src/app/__debug_bin: operation not permitted

OOps...

Итак добавим параметры:

$ docker run -it --rm --security-opt="apparmor=unconfined" --cap-add=SYS_PTRACE my-golang-app bash

И вуаля 🎉
$ root@7dc3a7e8b3fc:/go/src/app# dlv debug main.go
Type 'help' for list of commands.
(dlv)

P.S. опять же этот же трюк можно использовать с docker-compose/ либо с multi-stage билдами. Если интересно как дебажить multi-stage билды на Го просьба поставить “+” в комментариях или кинуть помидором 🍅.
Note #5 Дебажим приложения в multi-stage докере 🐳

Давайте представим, что на проде у вас есть микросервис, каждый из которых живет в своем Dockerfile и естественно, как у всех взрослых дядь - это multi-stage Dockerfile. Более подробно о multi-stage можно прочитать в доках ( https://docs.docker.com/develop/develop-images/multistage-build/), если лень - то этопросто Dockerfile у которого есть 2 FROM ключевых слова и в мы что-то копируем из одного в другой слой.

Итак приступим 🐎:
docker build -t goapp:latest .
Sending build context to Docker daemon 22.53kB
Step 1/7 : FROM golang AS builder
...
Successfully tagged goapp:latest


Каждый день нам нужно разрабатывать и дебажить это приложение: внимательный читатель может предложить установить дебаггер внутри одного из слоев. Итак добавим что-то вроде:


--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,5 @@
FROM golang AS builder
+RUN go get -u github.com/go-delve/delve/cmd/dlv
ADD . /src
RUN cd /src && go build -o goapp


И затем запустим что-то вроде этого:

➜  debug-multi-stage-docker-and-go docker run -it --entrypoint=dlv goapp
docker: Error response from daemon: OCI runtime create failed: container_linux.go:344: starting container process caused "exec: \"dlv\": executable file not found in $PATH": unknown.


И вот незадача, та же история будет если у вас docker-compose для локальной разработки или что-то еще. Как быть)
Есть замечательный флаг --target у docker build!

docker build --target builder -t goapp:latest .
docker run -it goapp sh
# dlv debug main.go
could not launch process: fork/exec /src/__debug_bin: operation not permitted

А как пофиксить это ☝️, читай мой прошлый пост https://yangx.top/golang_for_two/20 )

И Вуаля! Огромный Плюс ⚡️ данного подхода лично для меня, отсутствие Dockerfile-dev и вариантов.

P.S. Если же у тебя docker-compose, то в своем docker-compose.override.yml можно написать так:
version: "3.4"
services:
app:
image: goapp:dev
build:
context: .
dockerfile: Dockerfile
target: builder

🎉💥🎉
Note #6 Дебажим тесты 🐛🔥

Итак, часто нужно запустить 1 тест да еще и в режиме отладки, например, когда вы написали тест который повторяет баг. Все очень просто (хотя из доков не особо очевидно):

dlv test -- -test.run NameOfYourTest/PartOfTheName* (по сути тоже самое что и go test -run)

Или живой пример:

➜  debug_test dlv test -- -test.run TestFibonacciBig
(dlv) b main_test.go:6
Breakpoint 1 set at 0x115887f for github.com/andriisoldatenko/debug_test.TestFibonacciBig() ./main_test.go:6
(dlv) c
> github.com/andriisoldatenko/debug_test.TestFibonacciBig() ./main_test.go:6 (hits goroutine(17):1 total:1) (PC: 0x115887f)
1: package main
2:
3: import "testing"
4:
5: func TestFibonacciBig(t *testing.T) {
=> 6: var want int64 = 55
7: got := FibonacciBig(10)
8: if got.Int64() != want {
9: t.Errorf("Invalid Fibonacci value for N: %d, got: %d, want: %d", 10, got.Int64(), want)
10: }
11: }
(dlv)

Также можно запустить с -v (помним о go test -v):

➜  debug_test dlv test -- -test.v -test.run TestFibonacciBig
(dlv) c
=== RUN TestFibonacciBig
--- PASS: TestFibonacciBig (0.00s)
PASS
Designing Go code with interface chaining vs requiring deps.

https://gist.github.com/joncalhoun/0cd99c9082d2ba210c5169082038a420

Note: этот gist от создателя https://gophercises.com/, он написал в твиттере: времени нет писать 2 статьи - ловите код :)
В одном из недавних постов я писал, что люди требуют крови!: https://yangx.top/golang_for_two/16

CloudFlare написал long read на эту тему, что пошло не так -> https://new.blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019

Но самое интересно, там есть Gif в которой тестируют RegExp и Go комюнити в лице Nate Fintch прикинул, что можно написать так:

// Equivalent normal code is worst case len(s)+1 steps:
if !strings.HasSuffix(s, ";") {
return false
}
if !strings.Contains(s, "=") {
return false
}
Памятка Go разработчку про regexp!
Твит который набирает популярность!

src: https://twitter.com/eugen_yzeiri/status/1154400836862337025
В Go есть репозиторий "golang/proposal" который описывает процесс внесение изменений в язык. Так вот, Russ и ко. смекнули, (вероятно) после недавних зафейлиных предложений, что нужно что-то менять.
Мысли Russ'а можно прочитать тут, обещает опубликовать продолжение через пару дней🍿
💡Рубрика интересный факт:

Кто из вас слышал, что в Go до версии 1.0 был debugger под названием, барабанная дробь: OGLE.
Те задумка была такая:

> go ogle main.go

К сожалению, дебаггер не был готов к релизу go1.0 и его дропнули💩
Note #7 О стилистике Decode//UnMarshal 🐣

Недавно в твиттере возник вопрос, что лучше писать:

// Option A
var v T
v.Decode(someData)
Или так:
Option B
func (t *T) Decode(data []byte) {
// decode data into *t
}

На самом деле все зависит от контекста, а именно вариант В) или использование метода вместо функции - позволяет типам удовлетворять интерфейсам, т.е другими словами, если у вас есть где-то type Decoder interface{ Decode([]byte) error },
Если такого типа нет, оба варианта подойдут и как все знают на вкус и цвет фломастеры разные, хотя второй вариант более универсальный кмк.

Пару ссылок:
- Интересный gist о том как можно приводить типы между источниками
- Дока о том, что Decode - это обычно выражает Unmarshal BinaryUnmarshaler
Note #8 Ast T-shirt👕

Допустим вы решили себе напечатать футболку с куском кода который на Go, но hello-world это скучно. Вот вам идея, взять кусок синтаксического дерева из if err != nil. А если серьезно, то всегда интересно посмотреть - во что превратится ваша программа, когда компилятор ее парсит и строит ситаксическое дерево.

В общем получится что-то типа такого:
55  .  1: *ast.IfStmt {
56 . . If: 5:2
57 . . Cond: *ast.BinaryExpr {
58 . . . X: *ast.Ident {
59 . . . . NamePos: 5:5
60 . . . . Name: "err"
61 . . . . Obj: *(obj @ 38)
62 . . . }
63 . . . OpPos: 5:9
64 . . . Op: !=
65 . . . Y: *ast.Ident {
66 . . . . NamePos: 5:12
67 . . . . Name: "nil"
68 . . . }
69 . . }


Если чуток почистить, то можно и на чашку вместить:

&ast.IfStmt{
Cond: &ast.BinaryExpr{
X: &ast.Ident{Name: "err"},
Op: token.NEQ,
Y: &ast.Ident{Name: "nil"},
},
}


Ссылки:
- поиграться
- почитать
Слайды моего доклада “Advanced debugging techniques in different environments” на Kyiv Go Meetup July 2019:

- демо проект link
- слайды файлом link
- slideshare link

Вопросы пиши в личку: @a_soldatenko
Note #9 Когда лучше использовать именнованые параметры в функциях, определеных в интерфейсах?

Ели нам попадается такой код, то не совсем понятно что такое str или int O_O:

type runner interface {
run(context.Context, string, int)
}


Сразу же последует PR с чем-то вроде такого:

2c2
< run(context.Context, string, int)
---
> run(ctx context.Context, service string, instances int)
4d3
<

Так намного лучше :)

Но иногда есть более спорный кейс, в котором называть не объязательно:
type Enroller interface {
Enroll(*User, *Course) error
}


// P.S.
func main(){println("не забываем, что код пишется, еще и для наших коллеги, а не только для того, чтобы его можно было запустить на проде!")}

🍺
Note #10 Напоминание самому себе:

В Go можно делать вызов метода, напрямую используя тип:

package main

import (
"fmt"
)

type Person struct {
Name string
}

func (p Person) Say() {
fmt.Println("Go for two me and", p.Name)
}

func main() {
p := Person{"%username%"}
// Все варианты эквивалентны
p.Say()
Person.Say(p)
(Person).Say(p)
f1 := Person.Say; f1(p)
f2 := (Person).Say; f2(p)
}

links:
[1] https://play.golang.org/p/5TbFC1DsZyy
[2] https://golang.org/ref/spec#Method_expressions
Note #11: Go 2 и числовые литералы

Tl;dr в Go версии 2 собираются добавить некоторые изменения, которые затронут числовые литералы. Так это и так все знают: подумали Вы?
Давайте посмотрим, что же уже сейчас есть в go репо в главной ветке (master branch):

$ git clone [email protected]:golang/go.git
$ cd go/src && ./all.bash
$ go version devel +eee07a8e68 Wed Aug 21 15:20:00 2019 +0000 darwin/amd64

Один из самых заметных для меня это конечно _ в цифрах).

Небольшой примерчик:
package main

func main() {
println("1_200_000 -> ", 1_200_000)
println("3.1415_9265 -> ", 3.1415_9265)
println("0b0110_1101_1101_0010 -> ",0b0110_1101_1101_0010)
// println("0___ -> ", 0___) invalid example from discussion
// println(" 0__.0__e-0__ -> ",0__.0__e-0__) invalid example from discussion
// println("1__2", 1__2) invalid
}


Итак:
$ go version
$ go1.12.7 darwin/amd64
$ go run main.go
# command-line-arguments
./main.go:4:28: syntax error: unexpected _200_000, expecting comma or )
./main.go:5:35: syntax error: unexpected _9265, expecting comma or )
./main.go:6:39: syntax error: unexpected b0110_1101_1101_0010, expecting comma or )

Oops 😬


Попытка номер 2:
./go version 
devel +eee07a8e68 Wed Aug 21 15:20:00 2019 +0000 darwin/amd64
./go run main.go
1_200_000 -> 1200000
3.1415_9265 -> +3.141593e+000
0b0110_1101_1101_0010 -> 28114


Go 1.13RC1:
go get golang.org/dl/go1.13rc1
go1.13rc1 run main.go
1_200_000 -> 1200000
3.1415_9265 -> +3.141593e+000
0b0110_1101_1101_0010 -> 28114

🎉🎉🎉🎉

P.S. Не совсем понятно почему это вроде как должно быть в Go2, а попало в Go 1.13 … наверное я что-то упустил, пойду читать GIthub…

[1] https://go.googlesource.com/proposal/+/master/design/19308-number-literals.md
Note #12: defer statements in infinite loops

Длинный стек отложенных вызовов может занимать много памяти, как Вы уже догадались такой код будет стремительно течь 💦:

$ cat main.go

package main

func g()

func f() {
for {
defer g()
}
}

func main() {
f()
}


Отловить данную проблему поможет статический анализатор:

deferlint:
go vet -vettool=$(which deferlint) .
# github.com/andriisoldatenko/golang_for_two
./main.go:10:4: defer in loop found "defer g()"


либо staticcheck
staticcheck
main.go:10:4: defers in this infinite loop will never run (SA5003)


Иногда данная ситуация абсолютно валидная, поэтому staticcheck пытается найти циклы без условий или брейков, чтобы отметить такой кейс, это помогает бороться с ложными срабатываниями.

Кстати, deferlint - отличный пример, как написать свой линтер[1].
Исправить данную ситуацию возможно, обернув defer в анонимную функцию, чтобы гарантированно ваша отложенная функция была вызвана вовремя.

func f() {
for {
func(){
defer g()
}()
}
}


Потенциально, любой цикл, а особенно бесконечный, может служить причиной утечки памяти. defer следует гарантированно выполнять сразу же после того, как мы освободили ресурс, который нам уже больше не нужен. А иногда - это может происходить в цикле.

Links:
[1] Если вдруг вы пропустили статью о том, как написать свой линтер использую go/analysis, то вот https://arslan.io/2019/06/13/using-go-analysis-to-write-a-custom-linter/
Note #13: go doc with colors

Если вы используете go doc, для быстрого доступа к go документации, есть интересная идея как это все разукрасить:

func main() {
args := []string{"doc"}
args = append(args, os.Args[1:]...)
cmd := exec.Command("go", args...)
cmd.Env = os.Environ()

output, _ := cmd.CombinedOutput()

stdout := colorable.NewColorableStdout()
err := quick.Highlight(stdout, string(output),
"go", "terminal256", style)

stdout.Write([]byte("\033[0m\n"))
}

Код очень простой и не странно, что принимает те же параметры что и go doc :trollface:, как описал автор :)

Link: https://github.com/inancgumus/godocc

Установить можно так:
go get -u github.com/inancgumus/godocc

GODOCC_STYLE=monokai godocc time Unix
Note #14 argument reusing in fmt.Printf

Бывает необходимость напечатать несколько раз одно и тоже значение или значение и тип:
fmt.Printf("Num: %d %d\n", 2, 2)
// or
fmt.Printf("Num: %d %T\n", 2, 2)


Это легко переписать, используя индексы аргументов:
fmt.Printf("Num: %d\n", 42)
fmt.Printf("Num: %d %[1]d %[1]d %[2]d %[2]d", 2, 3)
}


more details: https://golang.org/pkg/fmt/
Note #15 Go и зарезервированные ключевые слова

Ключевые слова в Go можно переопределять, НО никогда не стоит так делать!
Такой код, как Вы уже догадались, вполне себе валидный код в Go:

package main

import (
"fmt"
)

func main() {
const iota = iota
fmt.Println(iota, iota, iota)
}
И мы получим в результате:😱: 0 0 0.

Как следствие:
const (
a = iota
b
c
)
fmt.Println(a, b, c)
0 0 0

И staticcheck или go vet ничего нам не сказали :(

И в конце, чтобы запутать своих коллег еще сильнее, возможно написать так:
type int struct{}

func main() {
var i int
i++
fmt.Println(i)
}

Компилятор подскажет ошибку:
invalid operation: i++ (non-numeric type int) o_O

P.S. SyntaxError: invalid syntax было бы лучше :)

Links:
[1] https://golang.org/ref/spec#Predeclared_identifiers
[2] https://play.golang.org/p/6OW0pJMvgQk
Note #16 GopherCon 2019 videos

Подвезли видео с GopherCon 2019 🎉🎉🎉

https://www.youtube.com/playlist?list=PL2ntRZ1ySWBdDyspRTNBIKES1Y-P__59_

Update: Лайтинг талкс: https://www.youtube.com/playlist?list=PL2ntRZ1ySWBedT1MDWF4xAD39vAad0DBT

P.S. пишите в личку ,если что-то по вашему мнению must have посмотреть, сделаем отдельный обзор! Enjoy!