Let My Gophers Go!
229 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.

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

"Если не учил физику в школе читал спецификацию Go, вся жизнь будет наполнена чудесами и волшебством"

var _ Fooer = (*Foo)(nil)

Строчка кода выше, если особо не задумываться, не кажется очень осмысленной. Blank identifier, nil, конвертирование типа — сразу несколько финтов, но ради чего?

Как это обычно бывает, ответ можно найти в Go Spec/Effective Go/Go FAQ. На этот раз — в последнем:

You can ask the compiler to check that the type T implements the interface I by attempting an assignment using the zero value for T or pointer to T, as appropriate:

type T struct{}
var _ I = T{} // Verify that T implements I.
var _ I = (*T)(nil) // Verify that *T implements I.

Несмотря на то, что в compile-time и так происходит проверка на реализацию нужного интерфейса, можно сделать это и самостоятельно при желании.
В примере выше происходит явная проверка на то, что типы T и *T реализуют интерфейс I.

При этом помним, что вторая проверка с (*T)(nil) сильнее: она точно окажется успешной, если T реализует интерфейс I. Обратное — неверно (см. пост выше про method sets).

Таким образом: var _ Fooer = (*Foo)(nil) конвертирует untyped nil в nil типа *Foo и пытается присвоить переменной типа I. Это, согласно спецификации, возможно, только если method set для конкретного типа в правой части assignment statement шире, чем method set интерфейса.

Можно было бы ещё реализовать проверку так:

var _ Fooer = &Foo{}
var _ Fooer = new(Foo)

Это, однако, приводит к ненужному в данном случае выделению памяти, поэтому Go FAQ даже их не упоминает.

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

Также Eli напоминает про возможность runtime-проверки:

var f Foo
_, ok := interface{}(f).(Fooer)

Конвертирование к типу interface{} (его реализуют все, потому что interface{} says nothing) нужно, так как type assertions работают только для интерфейсных типов.
7
Internals of Go

Меня очень впечатлила статья Расса про интерфейсы: с одной стороны, она достаточно подробная, но при этом легко читается и по стилю похожа на аннотированную спецификацию, где техлид проекта пытается объяснить, что и как работает.

Пытался найти что-нибудь подобное для просветления. Вот, что пока удалось найти:

Golang Internals Resources
Репозиторий с подборкой статей для тех, кто хочет знать, что происходит под капотом

Go Internals
Незаконченная книга по подкапотным деталям Go. Пока что показалась слишком хардкорной, судя по тому, что первая глава называется "assembly_primer". Надо набраться смелости и когда-нибудь ее все-таки прочитать :)

Вопрос на SO про изучение внутренностей Go
Отвечает господин Друзь один из самых активных участников SO в Go Language Collective — icza и приводит список must-read статей. Список частично перекликается с репозиторием в первом пункте.

Если есть что-то ещё на примете, кроме статей и видео по ссылкам выше — кидайте, будет круто :)
👍8
Go Gotchas

В своём первом посте делился ссылкой на Notion-страничку, где собираю классные ресурсы по Go. Один из блогов в списке — divan.dev, в котором есть неплохие статьи, например, с визуализацией concurrency.

В статье How to Avoid Go Gotchas автор формулирует следующую мысль: большая часть так называемых "gotchas" (грубо говоря, "ловушек") связана с непониманием чего-то фундаментального.

И дальше автор объясняет то, что, на его взгляд, обязательно нужно понимать, чтобы реже удивляться :)

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

Но один момент из статьи, несмотря на простоту, потребовал у меня времени на осознание: то, как работает append и что происходит в результате с копиями слайса.

Кажется, мой канал можно было смело назвать "Just read the specs" :)

If the capacity of s is not large enough to fit the additional values, append allocates a new, sufficiently large underlying array that fits both the existing slice elements and the additional values. Otherwise, append re-uses the underlying array.

Все просто: если len(slice) + len(data) > cap(slice), выделяется новый underlying-массив подходящего размера, готовый вместить в себя старые + новые элементы.

Что такое подходящий размер? Пока размер массива под капотом < 1024, будет выделяться в 2 раза больший массив, но после отметки в 1024 элемента — будет происходить увеличение размера в 1.25 раз.

Почему именно так? Отвечает Роб Пайк:

You need to pick something.

It was just arbitrary, I'm sure. 1024 is a nice number, and it's larger than the length of many slices.

Sometimes a number is just a number.

Иногда банан число всего лишь число. Справедливо :)

Append (немного) хитёр:

func main() {
s := make([]int, 0, 5)
s = append(s, []int{1, 2, 3, 4}...)

a := append(s, 5)
fmt.Println(a)

b := append(s, 6)
fmt.Println(b)
fmt.Println(a)
}

После таких нехитрых манипуляций получим:

[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 6]

С другой стороны, если поменять s := make([]int, 0, 5) на s := make([]int, 0, 4), получим:

[1 2 3 4 5]
[1 2 3 4 6]
[1 2 3 4 5]

Фрагмент из спецификации все объясняет: если capacity слайса хватает, исходный underlying-массив будет переиспользован, что и происходит в первом случае, где capacity = 5.

Во втором случае capacity = 4, что меньше len(slice) + len(data) = 4 + 1 = 5. В результате выделяется новый кусок памяти под массив для 8 элементов, а len и cap нового слайса принимают значения 5 и 8 (то самое удвоение), соответственно.

Поведение абсолютно понятное, но слегка коварное.

Интересно, что сам автор статьи в одном из примеров то ли опечатался, то ли тоже запутался:

a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42

Иллюстрация под кодом говорит о том, что len(b) = cap(b) = 15, но на самом деле: len(b) = 15, cap(b) = 31.

P.S. Кстати, солидно обновил список ресурсов по Go в Notion, если вдруг кто пропустил :)
🔥6👍4
Alias declaration vs type definition

type Test = string
type Test string

Если присмотреться, можно заметить, что это все-таки 2 разные строки :) При этом первая выглядит достаточно непривычно, в то время как вторая — конструкция, знакомая и понятная.

Идем за разъяснениями в спецификацию:

Alias declarations
An alias declaration binds an identifier to the given type.

Within the scope of the identifier, it serves as an alias for the type.

type (
nodeList = []*Node // nodeList and []*Node are identical types
Polar = polar // Polar and polar denote identical types
)

Согласно спецификации, Polar и polar — один и тот же тип, мы всего лишь создали "алиас".

С другой стороны:

A type definition creates a new, distinct type with the same underlying type and operations as the given type, and binds an identifier to it.

The new type is called a defined type. It is different from any other type, including the type it is created from.

type (
Point struct{ x, y float64 } // Point and struct{ x, y float64 } are different types
polar Point // polar and Point denote different types
)

В случае с type definition мы объявляем новый тип (defined type), на основе выбранного underlying type.

В примере выше polar — defined type, Point — underlying type.

Таким образом, есть две формы декларации типа (type declaration):

1) Декларация алиаса (alias declaration)
2) Определение типа (type definition)

Синтаксически они отличаются лишь знаком равенства, но никакого смыслового равенства нет.

И остается один вопрос: зачем вообще использовать type aliases?

Обращаемся к выступлению Расса под названием "Codebase Refactoring (with help from Go)", где написано:

5.3. Type aliases
To enable gradual code repair during codebase refactorings, it must be possible to create alternate names for a constant, function, variable, or type. Go already allows introducing alternate names for all constants, all functions, and nearly all variables, but no types. Put another way, the general alias form is never necessary for constants, never necessary for functions, only rarely necessary for variables, but always necessary for types.

type OldAPI = NewPackage.API

Если подытожить: это явно не то, чем придется регулярно пользоваться, но такой синтаксический финт может пригодиться во время масштабного рефакторинга кодовой базы.
👍3
Maps in Go

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

Устроена она несколько сложнее, чем массивы (константную сложность надо заслужить), но это все ещё не rocket science, если не вдаваться в криптографию.

Вероятно, это не самая точная аналогия, но будем для простоты терминологии называть map —своего рода "интерфейсом", который определяет нужное для нас поведение и свойство: O(1) lookups (то есть, доступ по ключу) в среднем (это значит, что может быть и хуже, если выбранная хеш-функция звезд с неба не хватает).

В Go этот "интерфейс" реализован как hashmap. Эта структура данных используется в Go рантайме.

Согласно Go FAQ, реализация настолько хороша, что в большинстве случаев о ней не придется задумываться:

The same reason strings are: they are such a powerful and important data structure that providing one excellent implementation with syntactic support makes programming more pleasant. We believe that Go's implementation of maps is strong enough that it will serve for the vast majority of uses.

Но это нас не остановит, конечно же.

Сначала заметим, что map в Go — это указатель на runtime.hmap. Иначе говоря, это так называемый reference/reference type, но никакого pass by reference по-прежнему нет: копируется указатель, а не сами данные.

Это стоит иметь в виду, и об этом предупреждает Effective Go:

Like slices, maps hold references to an underlying data structure. If you pass a map to a function that changes the contents of the map, the changes will be visible in the caller.

Кстати, когда-то вместо map[K]V предполагалось использовать *map[K]V, но потом, как пишет Ian Lance Taylor, стало ясно, что приходится всегда писать *map, это нудно, и поэтому синтаксис упростили.

Очень рекомендую статью Dave Cheney про устройство `map`. В ней рассказано:

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

— В каком виде map присутствует в ряде популярных языков

— Какие есть нюансы реализации в самом Go: например, при их реализации обошлись без дженериков, interface{} и кодогенерации

Если нужно для собеседований или по какой-то другой причине глубже разобраться в структурах данных и основах теории сложности алгоритмов (все эти о-малые, О-большие, теты и омеги), могу порекомендовать книгу Скиены — Algorithm Design Manual. Как по мне, идеальный баланс между теорией и практикой.
👍7
Maps in Go (addendum)

Кстати, в статье Dave Cheney есть один момент, который меня немного смущает.

В примерах применения хеш-функции говорится о "masking off the lower 3 bits", что по сути означает (должно означать в моем понимании) операцию AND с 111, но полученные номера бакетов почему-то не соответствуют такой логике (см. примеры из статьи).

Возможно, речь о чем-то другом и тогда буду благодарен, если кто-то в комментариях разъяснит :)