Как стать автором
Обновить

Комментарии 41

К Подольскому можно по-разному относиться как к человеку, но как Go программист он хорош, так что я бы не сравнивал его материал с вот этой ерундой уровня Junior --, да ещё и перекатившегося из Java/C#/PHP с любовью к худшим практикам (исключения, ORM).

Вкатышки этого не знают (т.к. и "преподаватели" курсов этого не знают), но вообще-то nil slice - полнофункциональный слайс, с len и cap равными 0. Его можно, а иногда и нужно использовать. Его вполне можно передать, а ещё append работает с ним как и с любым другим слайсом.

Говорить что map внутри - хэш таблица, это сильное упрощение. Она появилась сильно до дженериков, поэтому сделана в духе Си - куча unsafe указателей и колдунства вокруг них. Ну и да, порядок не гарантирован не из-за структуры map, а из-за того что он там явно рандомизирован. Ну кто код может прочесть - посмотрит.

Говорить что в Go нет lock-free структур данных и топить за mutex это значит полностью не понимать CSP и идиомы Go связанные с многопотоком. Ещё раз: гуглим go proverbs, читаем первую. Там будет "Don't communicate by sharing memory, share memory by communicating". Это трудно объяснять тем, кто уже впитал плохое из других языков, но я попробую. Вы пишите горутину так, чтобы она сделала своё грязное дело, потом вычитала map (ну или slice или что-то ещё) из входящего канала, записала результат своей работы, а потом передала этот map дальше в исходящий канал, следующей, точно такой же горутине. Всё. map в любой момент времени будет только в одной горутине, к ней вообще не будет конкурентного доступа. Блокировка происходит автоматически при попытке чтения из канала. Так же автоматически и снимается. Передача по каналу - zero copy, так что оверхед небольшой. Это - то зачем нужны каналы.

Речь про передачу всех данных мапы в канал копированием или только ее описания? Как сами пишете, её реализация - куча unsafe указателей, если просто написать

chan map[string]struct{...} то будет передан только указатель на мапу в канал. Блокировка будет только на получение указателя на мапу в горутину, нет? Не ковырял детально такое.. стараюсь в каналах гонять что-то по-проще..

Вот вы как раз и не поняли, что хотел сказать mrobespierre.


Блокировка будет только на получение указателя на мапу в горутину

Нет, явной блокировки на доступ к мапе вообще никакой не происходит в этом примере. Структура программы определяется так, что "барьером", который контролирует доступ, является (блокирующий) канал.


Т.к. каналы блокирующие, то конкретно в этом примере не будет ситуации, когда несколько горутин имеют доступ к одной мапе. Технически, это не lock-free, а практически блокировку явно никто не делает.

Говорить что в Go нет lock-free структур данных и топить за mutex это значит полностью не понимать CSP и идиомы Go связанные с многопотоком. Ещё раз: гуглим go proverbs, читаем первую. Там будет "Don't communicate by sharing memory, share memory by communicating".

Задача: Реализовать структуру-счетчик, которая будет инкрементироваться в конкурентной среде. По завершению программа должна выводить итоговое значение счетчика.

Условие: Решение без применения примитивов из пакета sync, исключительно используя канал для обеспечения потокобезопасной передачи/приёма данных.

Пытаюсь научиться лучшим практикам - никак. Помогите!

package main

import "sync"

type Counter struct {
	data  chan int
	total int
}

func NewCounter() *Counter {
	data := make(chan int)
	c := Counter{data, 0}
	go func() {
		for {
			increment := <-data
			c.total += increment
		}
	}()
	return &c
}

func (c Counter) Add() {
	c.data <- 1
}

func main() {
	counter := NewCounter()
	var wg sync.WaitGroup
	wg.Add(1000000)
	for i := 0; i < 1000000; i++ {
		go func() {
			counter.Add()
			wg.Done()
		}()
	}
	wg.Wait()
	println(counter.total)
}

В вашем коде data race. `wg.Wait()` дожидается пока все горутины закончат записывать в канал, но на момент вызова печати не гарантируется, что фоновая горутина, которая тащит данные из канала и суммирует в `total` закончила чтение

Спасибо! Исправил:

package main

import (
	"fmt"
	"sync"
)

type Counter struct {
	data  chan int
	total chan int
}

func NewCounter() *Counter {
	data := make(chan int)
	total := make(chan int)
	c := Counter{data, total}
	go func() {
		var count int
		for {
			select {
			case increment := <-data:
				count += increment
			case total <- count:
			}
		}
	}()
	return &c
}

func (c *Counter) Add(v int) {
	c.data <- v
}

func (c *Counter) Total() int {
	return <-c.total
}

func main() {
	counter := NewCounter()
	var wg sync.WaitGroup
	wg.Add(1000000)
	for i := 0; i < 1000000; i++ {
		go func() {
			counter.Add(1)
			wg.Done()
		}()
	}
	wg.Wait()
	fmt.Println(counter.Total())
}

Внутри канала тоже mutex, кстати.

Мда, на ночь глядя комменты на Хабре сочинять это конечно так себе идея:

Осваиваю профессию Prompt Engineering. Это ответы на вопросы. Мопед не мой. Спасибо, Codeium.

Автор в явном виде указывает что сам ничего не сочинял, а натравил очередной GPT на ту самую статью. Ну а то что получилась лажа так это не удивительно, если понимать, что у всех этих LLM под капотом.

Да почему "лажа"? Это отправная точка. Если относится к LLM с фигой в кармане, то можно получать некоторую выгоду.

Отсутствуют более весомые недостатки Go, такие как:

позднее включение в работу GC, настроенное по умолчанию на 1.5секунды или около того. Сколько раз отработает микросервис за это время, сколько памяти оторжут его копии?

Откровенно слабый компилятор, выбрасывающий в кучу локальные объекты, не способный инлайнить одноркатно примененную функцию больше 3- операторов, не способный инлайнить методы из даже одного оператора если применен defer и т.д. К счастью, есть escape-анализ и ряд иных фич, позволяющих понять что это ни разу не быстро.

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

Избыточное и вынужденное применение запятых. Хотелось избавиться от точки ч запятой, ура их нет! Зато теперь каждая третья строчка кода заканчивается на запятую.

Применение выражений присваивания в операторах ветвления и др. завершающихся точкой запятой

Передача всего исключительно по значению.

Кмк, неудачный синтаксис реализации дженериков, применение [] несколько путает код, особенно при не достаточно удачном нейминге слайса функций.

Дополню передачу по значению:

Такие структуры данных как строка, слайс, мапа .. передаются тоже "по значению", где оно представляет из себя .. копию описателя этой структуры. Но, поскольку под кампотом лежат указатели, то возможны неожиданные трюки и даже утечки, если данные реаллоцируются внутри функции/метода: в частности применение append к слайсу.

Неправильно.

Строки immutable типы. Для них эти проблемы неактуальны вообще.

Map это reference тип. По-значению у него передается указатель на саму структуру внутреннюю. Копии никакие не делаются.

Слайс - да, это единственный встроенный тип в Go, который имеет value семантику и копирует свое внутреннее представление. Есть мелкие особенности из-за этого (тот самый append), которые на практике проблем не доставляют. Зато профита от value семантики выше крыши. Если бы это был reference тип, то было бы в разы хуже. Неудивительно, что мейнстрим языки точно так же слайсы реализуют.

Не надо так делать (ибо есть иные инструменты), но: передаем строку в функцию, как бы иммутабл, как-бы всё штатно. Но строка - содержит в себе тот же самый unsafe указатель под капотом. Меняем указатель внутри или содержимое через тот же unsafe и алга! Ещё раз: такое делать не надо, но .. не запрещено языком явно, хотя immutable декларирован.

мапа - как структура данных, это да, указатель. Под капотом много разных структур + рандомизатор. Передаем мапу, ожидаем ее константность (передача исключительно по значению ведь) забыв что это "указатель" и алга. Видел и такое у начинающих в Go.. зачем тут указатель на мапу? Дык, дабы не по значению.. ;)

В слайсе это просто проявляется явно и часто. И там не только append..

Особенно иногда доставляет огромное удовольствие делать сортированные мапы .. ;) Но, с мапами вообще стараюсь не работать и в большинстве "нативных" применений они не нужны на моей практике.. дорого это всё.

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

Менять строки запрещено спецификацией языка. Там четко написано, immutable. Все отхождения от нее это неопределенное поведение. Строка может быть литералом, который указывает на read-only секцию в бинаре. Если попробуешь поменять, то получишь SIGBUS или SIGSEGV какой-нить. Если строка таки на хипе, то поведение уже вполне себе неопределенное. unsafe на то и unsafe, что он позволяет нарушать спеку и инварианты языка. Прямо как Rust.

Подобные ошибки с мапами могут совершать только совсем зеленые гошники. Всем известна простая истина - в Go абсолютно все передается по значению. И в случае мапы значение это адрес на структуру. Из этого вытекает все остальное. Тоже самое с каналами.

Это мелочи, но их должен знать даже джун. И это не является недостатками или преимуществами. Это просто особенности языка, которые есть у всех. Уж value/reference семантика так подавно. Это вполне себе обычная вещь, знакомая любому C/C++/C# программеру. С памятью надо уметь работать и Go выбрал путь, где программистам доступны некоторые подробности работы с памятью.

Ну ок, уболтали. Давайте вычеркнем строки из списка. ;)

В Го приходят не только с православно-правильного С/С++ и т.д., но много приходит с PHP или Python. Для них понятие ассоциативный массив, словарь - вполне рабочие и привычные явления. Многие тонкости (а это именно тонкости) мап, слайсов не так очевидны, тем не менее. Это как раз вопросы собеседований, для получения фидбека: понял ли испытуемый правильно эти структуры или нет. Почему и вынес в комментарий. Ну и уже не раз видел на практике миддлов, которые делали круглые глаза "а чо так бывает?" ;)

Кмк, "правильный" подход - это явное указание IN, OUT, INOUT параметров.. хотя бы через модификатор const ;)

Всем известна простая истина - в Go абсолютно все передается по значению.


А вот что рассказывает робот по теме "Ссылочные типы данных":

Ссылочные типы данных в GoLang - это типы данных, которые хранятся в системной куче (heap) и передаются по ссылке, а не по значению. Это означает, что при передаче ссылочного типа данных в функцию, функция работает с оригинальным объектом, а не с его копией. Некоторые из ссылочных типов данных в GoLang:

  • Срезы (slices) - это динамические массивы, которые представляют собой ссылку на последовательность элементов определенного типа.

  • Карты (maps) - это ассоциативные массивы, которые представляют собой ссылку на набор пар ключ-значение.

  • Каналы (channels) - это механизм для обмена данными между горутинами (goroutines) в многопоточной программе.

  • Указатели (pointers) - это переменные, которые хранят адрес в памяти другой переменной.

  • ?? Структуры (structs) - это пользовательские типы данных, которые могут содержать поля разных типов.

  • Интерфейсы (interfaces) - это типы данных, которые определяют набор методов, которые должны быть реализованы для типа данных, чтобы он удовлетворял интерфейсу.

  • Функции (functions) - это типы данных, которые могут быть переданы в качестве аргументов другим функциям или возвращены из функций.

Все эти типы данных являются ссылочными в GoLang и передаются по ссылке, а не по значению.

Нельзя сравнивать []int{1,2,3} == []int{1,2,3}, в отличии от [3]int{1,2,3} == [3]int{1,2,3}

Поправка:

В GoLang тип данных struct является составным типом данных, который объединяет несколько полей разных типов данных в один объект. struct не является ссылочным типом данных, а является значимым типом данных, то есть при передаче struct в функцию или присваивании его переменной происходит копирование значений его полей. Однако, при передаче struct в функцию в качестве аргумента, происходит передача его копии, что может быть неэффективно для больших struct. В таких случаях можно использовать указатели на struct.

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

В GоLang нет ссылок, только указатели (ссылки есть в c/c++). У переменой есть значение, адрес ссылки это адрес значения, а указатель на значение имеет свой собственный адрес (​это очень грубое объяснение).

Спасибо, про некоторые моменты не знал. За это и люблю комментарии на хабре)
1. Про полторы секунды до запуска GC не знал от слова совсем. С одной стороны в обычной работе это быть проблемной не должно, но в случае падения сервиса, может произойти так, что из-за этой задержки сервис будет падать и падать, что в целом может привести к отказу всех остальных инстансов. Я правильно понимаю? Были ли у вас случае в проде, когда это действительно было проблемой? И не поможет ли запуск хартбита с задержкой как минимум 1.5-2 секунды (хотя обычно столько занимает поднятие сервера, подключение к хранилищам и тд)
2. Имхо, но запятые это вкусовщина
3. Передача по значению - когда переходил с питона, то было не очень удобно, со временем привык, ide в целом это закрывают. Хотя, если затронули вопрос передачи аругментов, то хотелось бы увидеть опциональные поля, проверка полей в структурах-конфигах не самая приятная процедура, когда полей там 10-20.
4. С рефлексией полностью согласен, но насколько я знаю (хотя могу и ошибаться), сейчас разработчики go пишут новые стандартные библиотеки на основе дженериков, правда сколько времени займет переписывание большего функционала go - впорос.
5. Дополню пункт про передачу слайса, мапы и тд. На самом деле это далеко не самые опасные случаи, про них практически все знают. На что действительно стоит обращать внимание, так это на структуры, в которых есть указатели, и на структуры, с мапами и слайсами(для таких структур я в 90% случаев стараюсь передавать сразу указатель на структуру)
6. Но немного не согласен со строками, насколько я знаю строки - это массив символов. Единственный момент, что go нам не позволяет видоизменять этот массив, а при присвоении другого значения в переменную, мы просто создаем новую строку в памяти.

Если в каких-то моментах ошибся, то буду рад, если меня поправят.

У меня был эпизод, когда сервис разматывал только логов на 1.5 метра за доли секунды, и было не ясно "куда он жрет столько памяти?". Да, к счастью режим debug, логи по большей части трейсовые, но заинтересовало, полез копаться.. накопал. ;)

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

  1. Именно! Когда язык не однороден - тут значение, а тут типа тоже "значение", которое указатель, появляется много не очевидных моментов в таких структурах.

  2. Нет. Строка - такой же объект по типу слайса. Есть "описатель строки", содержащий под капотом указатель на сам текст..

Именно! Когда язык не однороден - тут значение, а тут типа тоже "значение", которое указатель, появляется много не очевидных моментов в таких структурах.

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

Верно. Только есть базовые типы данных, структуры .. а есть "указательные", такие как мапы, слайсы, строки.. :)

"Огласите весь список, пожалуйста!"

slices, maps, channels, pointers, interfaces, functions, strings.

позднее включение в работу GC, настроенное по умолчанию на 1.5секунды или около того. Сколько раз отработает микросервис за это время, сколько памяти оторжут его копии?

Скорее всего нисколько. В продакшен системах дольше этих 1.5сек будут отрабатывать хелсчеки банальные. Эта особенность не стоит даже упоминания.

И вообще неплохо бы пруф. Про подобное поведение слышу в первый раз. Чисто любопытства ради знать полезно было бы, если это так. Не более.

Пруф находил в рантайме, это не трудно.. ;)

Почему для “type parameters” квадратные скобки, когда угловые для генериков в Java, C#, C++, TypeScript и Dart?

Вероятно, по аналогии с объявлением типа для ключей мапы: map[int]bool

И чтобы дистанцироваться от генериков в других языках, т.к. нет возможности создавать классы или интерфейсы с “type parameters”. Вместо этого можно использовать обобщенные функции и методы.

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

В каком-то комментарии выше ответили, что эта статья написана в ответ на другую статью. Другой вопрос, что все вопросы, которые там(и здесь) перечислены, построены таким образом, что каждый можно раскрывать минут по 10, дополняя краевыми случаями. Банально устройство мапы и синхронизация - казалось бы, если мы уверены, что будем писать в параллельно только уникальные значения, то может обойтись без синхронизации? Нет, не можем, по той причине, что, когда бакеты будут заполнены в среднем на 6.5(размер бакета 8, если я правильно помню), то будут пересчитаны новые бакеты и запущен процесс перебалансировки, а в этот момент, другая горутина может писать в мапу, что в конечном итоге вызовет панику.

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

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

Если хочется подробностей, у Go есть модель памяти. Там описано, что делать можно, а что нет. Даже если бы у мапы не было перебалансировки, все равно делать так было бы нельзя. Мапа это большая структура. Операции с ней не будут атомарны и потоки будут видеть промежуточное некорректное состояние. Даже если мы один флажок пишем только в одном потоке, все равно нужны примитивы синхронизации, хотя бы какие-то. Барьер какой-нить или что-то, что и компилятору, и процессору намекнет, как правильно себя вести.

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

Это отправная точка. Если относиться к LLM с фигой в кармане, то можно получать некоторую выгоду.

Осваиваю профессию Prompt Engineering. Это ответы на вопросы. Мопед не мой. Спасибо, Codeium.

https://codeium.com/

Codeium · Free AI Code Completion & Chat

Не уверен, что подавать вывод LLM как обучающую статью - это хорошая идея. Сразу же возникают очень большие вопросы к корректности ответов на вопросы.

(и в целом - не уверен, что хотел бы видеть на Хабре статьи, большая часть которых сгенерирована LLM)

На stackoverflow в какой-то момент решили гасить весь контент, сгенерированный GPT/LLM. Потом правда вроде передумали

Потом правда вроде передумали

И зря.

Кошмар для UGC сайтов. Но фарш невозможно провернуть назад. Надо учиться теперь с этим жить.

Внедрили свой OverflowAI

Наводящие вопросы: какая hash-функция используется в map в Go?

*мем с гусём*

Так какая там хеш-функция?

Какая хеш-функция, спрашиваю!!!

:-D

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории