Let My Gophers Go!
230 subscribers
3 photos
22 links
Пришел в Go из Python и Data Science без особого бэкграунда в C/C++/Java.

Можно с натяжкой сказать, что пишу на Go в течение нескольких месяцев, и поэтому все написанное здесь стоит воспринимать как попытку разобраться и поделиться мыслями в процессе
Download Telegram
Channel created
Список полезных ресурсов для изучения Go

https://www.notion.so/wdesert/Let-My-Gophers-Go-29d7e8fe712141cf8ac39b84350f0db7

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

Список может кому-то показаться полезным. В ближайшее время планирую добавить туда несколько классных постов
🔥3
Копирование T, когда методы определены с *T — получателем

6.2 Methods with a Pointer Receiver

If all the methods of a named type T have a receiver type of T itself (not *T ), it is safe to copy instances of that type; calling any of its methods necessarily makes a copy. For example, time.Duration values are liberally copied, including as arguments to functions. But if any method has a pointer receiver, you should avoid copying instances of T because doing so may violate internal invariants. For example, copying an instance of bytes.Buffer would cause the original and the copy to alias ( §2.3.2 ) the same underlying array of bytes. Subsequent method calls would have unpredictable effects.

(The Go Programming Language Alan A. A. Donovan · Brian W. Kernighan)


Если резюмировать:

— Копирование инстансов типа T, если все методы определены с получателем типа *T, безопасно

— Если же есть хоть один метод типа func (*T), подобное копирование следует избегать, потому что возможны "неожиданные" изменения внутренних инвариантов.

В качестве примера приводится bytes.Buffer, который выглядит так:

type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}

Копирование инстансов типа bytes.Buffer создает алиасы для одного и того же массива под капотом слайса. Последующие вызовы методов — а они как раз определены как func (b *Buffer), могут привести к неожиданным результатам.

С другой стороны, мне по-прежнему кажется, что стоило бы в этом параграфе сделать маленькую оговорку: даже отсутствие методов с получателем типа *T не гарантирует отсутствие внезапных изменений.

Если речь идет о так называемых reference-типах (например, map или slice), независимо от методов мы можем изменить структуру данных, на которые ссылается указатель.

Кстати, Роб Пайк предлагал про это поведение дополнительно рассказать в спецификации:
https://github.com/golang/go/issues/5083

В целом, не мне, конечно, критиковать то, что написали Донован и Керниган, но есть ощущение, что на самом деле имелось в виду следующее: если в API методы определены с value-получателем, это означает/намекает/подсказывает (но формально НЕ гарантирует), что копирование безопасно и не приведет к нежелательным мутациям в структурах данных.

А это ещё один повод перечитать пост Билла Кеннеди про data & semantics:
https://www.ardanlabs.com/blog/2017/06/design-philosophy-on-data-and-semantics.html
О канале

Кто-то однажды сказал, что лучший способ разобраться в предмете — не задать вопрос, а опубликовать абсолютно неверный ответ :)

Цели вводить в заблуждение у меня, конечно, нет, но мотивация похожая. Как понятно из описания канала, ни о какой авторитетности с моей стороны и речи быть не может. В канале я буду писать обо всем, в чем мне лично хотелось бы разобраться/не кажется очевидным. Я достаточно усерден (а иногда и чрезмерно дотошен) в плане ресерча и устранения неопределенности, поэтому таких моментов может быть достаточно много.

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

Другое дело — записывать как сам вопрос, так и свой "ответ" с результатами маленького исследования вопроса, к которым всегда можно вернуться. В общем, даже самый тупой карандаш лучше самой острой памяти :)

Прошу воспринимать все написанное здесь максимально строго и критично. Любой фидбэк крайне приветствуется, ведь именно это и является целью канала.

В свою очередь, буду периодически делиться всем полезным и интересным, что нахожу про Go (или не только). Читаю я регулярно, много, на двух языках, поэтому можно ожидать обновлений.

Let My Gophers Go! :)
Let My Gophers Go! pinned «О канале Кто-то однажды сказал, что лучший способ разобраться в предмете — не задать вопрос, а опубликовать абсолютно неверный ответ :) Цели вводить в заблуждение у меня, конечно, нет, но мотивация похожая. Как понятно из описания канала, ни о какой авторитетности…»
Pass by value

Интересно, что в некоторых туториалах и постах про Go по-прежнему пытаются объяснить поведение кода, используя фразу "passed by reference".

Go FAQ весьма недвусмысленно констатирует:

As in all languages in the C family, everything in Go is passed by value

Нет никакого pass by reference в Go, о чем кратко, но очень понятно написал Dave Cheney:
https://dave.cheney.net/2017/04/29/there-is-no-pass-by-reference-in-go

Наверное, именно эта статья навсегда закрыла этот вопрос для меня и отбила желание искать некорректные аналогии.

С другой стороны, раз уж я пришел в Go из Python, стоит ещё отметить интересный момент: в Питоне тоже все передается по значению, с одной маленькой ремаркой: каждое значение является референсом.

В английском это звучит более складно и смешнее: everything in Python is passed by value, but all values are references :)

Я бы позволил себе смелость заявить, что Питон в каком-то смысле делает выбор за программиста и лишает возможности выбрать (и тут в канал влетает Bill Kennedy) семантику. При всей любви к Питону, мне больше нравится иметь возможность самому сделать выбор, а также лучше понимать, что на самом деле будет происходить.
👍2
Zen of Go

Очень велик соблазн узнать, как писать идиоматичный, каноничный код на Go (да и вообще в целом).

Этот вопрос неоднократно появляется у меня, неоднократно всплывает на reddit.com/r/golang, и ответ обычно сводится к следующему:

— Прочитать Effective Go
— Прочитать Go Code Review Comments
— Посмотреть и прочитать Go Proverbs
— Почитать исходники
— Обратить внимание на upspin (авторитетно, потому что у истоков стояли Rob Pike и Andrew Gerrand)
— Обратить внимание на проекты в репозитории Hashicorp (не берусь судить), но их часто рекомендуют в качестве "хорошего кода на Go"

Обычно я почти ко всем источникам возвращаюсь по мере усвоения информации, и в этом случае происходит то же самое: не один раз перечитал Effective Go и Go Code Review Comments (и продолжаю, безусловно) — и каждый раз кажется, что в i+1-й раз понял написанное лучше или вообще иначе, нежели в i-й.

Но за всеми рекомендациями и "пословицами" в мире Go, которые сформулировал Роб Пайк, стоят более крупные сущности — ключевые ценности и принципы языка и экосистемы. Так называемый "Go-дзен".

Питонисты знают о "пасхалке" в виде import this, которая содержит "дзен Питона", разработанный Тимом Питерсом. Например:

— Beautiful is better than ugly
— Explicit is better than implicit
— Simple is better than complex, etc.

Dave Cheney (кажется, я, как и многие, его фанат) очень интересно проводит параллели и пытается найти соответствующие идиомы и принципы в Go. Примеров кода почти нет, но все равно познавательно.

https://dave.cheney.net/2020/02/23/the-zen-of-go

P.S. Возьму на себя смелость и скажу, что в какой-то момент кажется, что Go в большей степени соответствует "дзену" Питерса, чем сам Питон 🤷‍♂️
👍2
Go Method Sets

Уже в Tour of Go можно узнать, что Go достаточно либерален в плане вызова методов, поэтому все 4 вызова ниже возможны:

type Point struct{ X, Y float64 }

func (p Point) Distance(q Point) float64 {
...
}

func (p *Point) ScaleBy(factor float64) {
...
}

p := Point{1, 2}
q := Point{1, 1}
pptr := &Point{1, 2}

p.Distance(q)
p.ScaleBy(2)

pptr.Distance(q)
pptr.ScaleBy(2)

Причина достаточно проста и описана в спецификации:

The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T)

A method call x.m() is valid if the method set of (the type of) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x's method set contains m, x.m() is shorthand for (&x).m()

Если коротко, "множество методов" для типа *T включает в себя множество методов, определенных как с T, так и *T получателем. Это логично, ведь всегда можно получить значение, разыменовав указатель. Поэтому на самом деле вызов pptr.Distance(q) будет преобразовано в (*pptr).Distance(q).

Более того, для addressable типов можно не писать (&x)., компилятор сделает это за нас.
Как говорит Bill Kennedy, "Go loves you" :)

Тем не менее, поведение ниже меня в первый раз удивило:

type Vectorizer interface {
Distance(q Point) float64
ScaleBy(factor float64)
}

var _ Vectorizer = p


./prog.go:33:6: cannot use p (type Point) as type Vectorizer in assignment:
Point does not implement Vectorizer (ScaleBy method has pointer receiver)

Текст ошибки весьма однозначно описывает проблему: "... Point does not implement Vectorizer (ScaleBy method has pointer receiver)". Починить достаточно просто — var _Vectorizer = &p, но интереснее всего понять причину ошибки.

Читаем спецификацию:

A value x is assignable to a variable of type T ("x is assignable to T") if one of the following conditions applies:

...

T is an interface type and x implements T

Даже это ещё не все. Даже если бы такое присвоение было корректным, не выполняется условие из параграфа Method calls: If x is addressable...

Снова читаем спецификацию:

The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal. If the evaluation of x would cause a run-time panic, then the evaluation of &x does too.

Интерфейсы не являются addressable типом.

Отдельный пункт про method sets есть и в Go FAQ:

Even in cases where the compiler could take the address of a value to pass to the method, if the method modifies the value the changes will be lost in the caller. As an example, if the Write method of bytes.Buffer used a value receiver rather than a pointer, this code:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)
would copy standard input into a copy of buf, not into buf itself. This is almost never the desired behavior.

В отличие от предыдущего случая, здесь имеем переменную buf, которая (см. цитату выше) является addressable. Потенциально компилятор может взять адрес переменной и совершить свою маленькую магию, но это будет означать, что копирование произойдет в копию buf, а исходная переменная никакие изменения не увидит.

Передаем указатель сами и все снова работает: io.Copy(&buf, os.Stdin)

Effective Go этот момент тоже комментирует:

This rule arises because pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded. The language therefore disallows this mistake.

Теперь кажется, что стоило всего лишь раз внимательно прочитать спецификацию, но разве герои идут в обход :)
🔥6👍3
Clean Architecture: The Go Way

"Clean architecture" — философский камень программирования, про который почти регулярно спрашивают на reddit.com/r/golang, а ответ обычно сводится либо к (отчасти необъяснимой) ненависти к Uncle Bob, либо к раздражению в связи с попыткой чрезмерно усложнить Go и привнести в него кусочки Java.

Саму книгу я несколько раз честно пытался читать, но все время было ощущение, что можно было вполне обойтись одной статьей.

Про чистую архитектуру в Go написано немало, но от некоторых постов и якобы "правильных" реализаций складывалось ощущение, что это действительно слишком сложно, абстракции ради абстракций. В общем, Роб Пайк бы не одобрил. Simplicity is complicated.

Какие ресурсы показались мне понятными/адекватными/занятными:

https://appliedgo.net/di/
Статья от Christoph Berger про dependency injection (когда-то я купил курс Кристофа Applied Go, который в целом очень рекомендую). Никаких революционных мыслей, немного игрушечные примеры, но все очень понятно и доступно, с мотивацией, когда в конце поста ты не просто запомнил классные английские словосочетания, но ещё и понял, зачем это применять на практике.

https://dave.cheney.net/2016/08/20/solid-go-design
Dave Cheney размышляет о принципах SOLID в контексте Go.

https://www.calhoun.io/moving-towards-domain-driven-design-in-go/
Jon Calhoun достаточно известен своими курсами, а в этой статье размышляет на тему DDD и Clean Architecture.

https://changelog.com/gotime/102
Очень крутой эпизод Go Time с Peter Bourgon (который недавно стал persona non grata из-за чрезмерной любви к троллингу), Kat Zien и Ben Johnson.

https://medium.com/@benbjohnson/standard-package-layout-7cdbc8391fc1#.o681bumjb
Кстати о Бене Джонсоне: этот его пост обычно первым делом рекомендуют в ответ на вопрос об организации кода.

https://www.ardanlabs.com/blog/2017/02/design-philosophy-on-packaging.html
Куда же без мнения Билла Кеннеди.

https://threedots.tech/go-with-the-domain/
Бесплатная книга Go With the Domain — объясняет DDD и Clean Architecture в процессе рефакторинга реального приложения (только начал читать, если книга — отстой, пишите в комментариях)

Интересно, что, если загуглить "Go project layout", первой же ссылкой видим Standard Go Project Layout . И тут же второй ссылкой — тикет от Russ Cox, где Расс сожалеет о том, что эту ссылку многие считают официальной рекомендацией :)

Если знаете другие классные на ваш взгляд материалы, буду благодарен, если поделитесь в комментариях 😊
👍9🔥1
Functional Options in Go

Прочитал статью про различные способы инициализации структуры и вспомнил про другую от Dave Cheney.

В этот раз написал пост в виде странички Notion, потому что там и писать удобнее, да и выглядит аккуратнее.

Поставьте 🤮 под этим сообщением, если читать так неудобно, или это привело к тому, что вы даже не стали открывать страницу :)

https://wdesert.notion.site/Functional-Options-in-Go-ebb47f62f6ae46ffa3c76ec76aba645a
🤮15👍12
Занудство о методах и попытки понять Расса

Читал вчера статью Russ Cox об интерфейсах. Использую несовершенную форму глагола, потому что, несмотря на понимание в общих чертах, не могу сказать, что усвоил написанное (когда-нибудь придется сделать перевод-пересказ, чтобы проверить своё понимание).

Споткнулся, к своему сожалению, достаточно рано (см. изображение).

Когда речь зашла о списке указателей на функции, появилось следующее method expression (согласно спецификации): (*Binary).String.

Расс называет это function pointer. В Go функции — первоклассные граждане и референс-типы, поэтому явный указатель не нужен.

To call s.String(), the Go compiler generates code that does the equivalent of the C expression s.tab->fun[0](s.data): it calls the appropriate function pointer from the itable, passing the interface value's data word as the function's first (in this example, only) argument.

Суть написанного в том, что при вызове нужного метода передается не само значение внутри интерфейса, а второе слово interface value — то есть, указатель на него.

И указатели на функции внутри itable (первое слово interface value) на самом деле имеют вид (*Binary).String.

Раздел спецификации про method expressions разъясняет:

type T struct {
a int
}
func (tv T) Mv(a int) int { return 0 } // value receiver
func (tp *T) Mp(f float32) float32 { return 1 } // pointer receiver

var t T

Вызов t.Mv(7) эквивалентен T.Mv(t, 7) и аналогично (&t).Mp(10) эквивалентен (*T).Mp(t, 10).

Для метода с value-получателем существует также сигнатура с указателем:

func(tv *T, a int) int, в которой подразумевается indirection/dereference (то есть, разыменование) указателя и вызов: func(tv T, a int) int.

Это как раз и гарантирует, что мы можем вызвать метод, определенный с value receiver даже для указателя — компилятор возьмет значение и вызовет нужную функцию.

Четвертый сценарий невозможен, так как множество методов для типа T не включает в себя методы, определеленные для *T. Исключение: для обычных вызовов (неинтерфейсных) компилятор в состоянии добавить (&t). за нас. Это возможно, правда, только для типов, у которых можно взять адрес (addressable), например, у переменной.

Если подытожить: вся магия в том, что из 4 возможных комбинаций value/value, value/pointer, pointer/value, pointer/pointer существует 3 сигнатуры функции, которые позволяют подстроиться. Исключением является только случай, когда мы пытаемся вызвать метод с pointer-получателем, передавая value type.

Это, конечно, все занудство и, очень вероятно, где-то выше я исказил то, что реально написано в спецификации и у Расса (прошу поправлять в комментариях, если это так).

В книге Go101 в разделе про методы про это тоже написано, но используется терминология, которой нет в спецификации и официальных блог-постах (method normalization, boxing/unboxing), а это немного смущает, хотя очень впечатляет подробность.

Заранее спасибо, если укажете на заблуждения, ошибки, неточности и другие интересные ресурсы :)
👍4🤔1
Hypothes.is

Небольшой оффтопик про чтение статей. Нашел прикольный инструмент, который позволяет делать аннотации и хайлайты в статьях и, что ещё более удобно, в PDF-файлах.

Очень актуально, когда нужно в чем-то разобраться, источников несколько (например, в случае выше — блог-пост, спецификация, ещё несколько статей) и понимание приходится складывать по кусочкам :)
👍2
Go Time!

Go Time — классный подкаст, посвященный Go и всему, что с ним связано. Иногда приходят даже члены Go Team и участвуют в AMA (ask me anything) сессиях.

Один из самых интересных эпизодов — Creating the Go Programming Language с участием Роба Пайка и Роберта Гризмера.

Мне не так легко в плане концентрации даются длинные подкасты, но, к счастью, у Go Time для каждого эпизода есть транскрипт.

Например, эпизод про создание Go можно не слушать, а прочитать здесь.

Помимо прочего, это ещё и отличная возможность начать активно слушать об интересной для себя тематике на английском. Все непонятное и "нерасслышанное" можно подсмотреть в скрипте и разобрать, если неизвестен перевод или правильное произношение.

Уже за несколько эпизодов можно сильно поднять как свой "технический" английский, так и восприятие на слух — а это очень пригодится на собеседовании, например. В общем, win-win.

Приятного прослушивания и чтения :)