-
Какой самый эффективный способ конкатенации строк?
Так как строки представляют собой неизменяемый слайс байтов, при конкатенации двух строк происходит выделение новой памяти. Это может негативно сказаться на потреблении памяти, если нужно соединить много строк. Поэтому в таких случаях следует использовать
strings.Builder
.var b strings.Builder b.Grow(100) for i := 0; i<100; i++ { fmt.Fprint(&b, "a") } fmt.Println(b.String()) // aaaaaaaa....
-
Что такое интерфейсы, как они применяются в Go?
Интерфейсы — это инструменты для определения наборов действий и поведения. Интерфейсы — это в первую очередь контракты. Они позволяют объектам опираться на абстракции, а не фактические реализации других объектов. При этом для компоновки различных поведений можно группировать несколько интерфейсов. В общем смысле — это набор методов, представляющих стандартное поведение для различных типов данных.
Как устроен Duck-typing в Go?
Если это выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, утка и есть.
Если структура содержит в себе все методы, что объявлены в интерфейсе, и их сигнатуры совпадают — она автоматически удовлетворяет интерфейс.
Такой подход позволяет полиморфно (полиморфизм — способность функции обрабатывать данные разных типов) работать с объектами, которые не связаны в иерархии наследования. Достаточно, чтобы все эти объекты поддерживали необходимый набор методов.
Интерфейсный тип
В Go интерфейсный тип выглядит вот так:
type iface struct { tab *itab data unsafe.Pointer }
Где
tab
— это указатель наInterface Table
илиitable
— структуру, которая хранит некоторые метаданные о типе и список методов, используемых для удовлетворения интерфейса, а data указывает на реальную область памяти, в которой лежат данные изначального объекта (статическим типом).Компилятор генерирует метаданные для каждого статического типа, в которых, помимо прочего, хранится список методов, реализованных для данного типа. Аналогично генерируются метаданные со списком методов для каждого интерфейса. Теперь, во время исполнения программы, runtime Go может вычислить
itable
на лету (late binding) для каждой конкретной пары. Этотitable
кешируется, поэтому просчёт происходит только один раз.Зная это, становится очевидно, почему Go ловит несоответствия типов на этапе компиляции, но кастинг к интерфейсу — во время исполнения.
Что важно помнить — переменная интерфейсного типа может принимать
nil
. Но так как объект интерфейса в Go содержит два поля:tab
иdata
— по правилам Go, интерфейс может быть равенnil
только если оба этих поля не определены (faq):var ( builder *strings.Builder stringer fmt.Stringer ) fmt.Println(builder, stringer) // nil nil fmt.Println(stringer == nil) // true fmt.Println(builder == nil) // true stringer = builder fmt.Println(builder, stringer) // nil nil fmt.Println(stringer == nil) // false (!!!) fmt.Println(builder == nil) // true
Пустой interface{}
Ему удовлетворяет вообще любой тип. Пустой интерфейс ничего не означает, никакой абстракции. Поэтому использовать пустые интерфейсы нужно в самых крайних случаях.
На какой стороне описывать интерфейс — на передающей или принимающей?
Многое зависит от конкретного случая, но по умолчанию описывать интерфейсы следует на принимающей стороне — таким образом, ваш код будет меньше зависеть от какого-то другого кода/пакета/реализации.
Другими словами, если нам в каком-то месте требуется «что-то что умеет себя закрывать», или — умеет метод Close() error, или (другими словами) удовлетворят интерфейсу:
type something interface { Close() error }
То он (интерфейс) должен быть описан на принимающей стороне. Так принимающая сторона не будет ничего знать о том, что именно в неё может «прилететь», но точно знает поведение этого «чего-то». Таким образом реализуется инверсия зависимости, и код становится проще переиспользовать/тестировать.
-
Чем отличаются
RWMutex
отMutex
?Mutex
позволяет блокировать доступ к общим ресурсам с помощью методовLock
иUnlock
. УRWMutex
есть еще два методаRLock
иRUnlock
, которые можно использовать, если общий ресурс нуждается в чтении, таким образом другие операцииRLock/RUnlock
не заблокируются, а операцииLock/Unlock
заблокируются. -
Чем отличаются буферизированные и не буферизированные каналы?
Чтение или запись данных в небуферизированный канал блокирует горутину и контроль передается свободной горутине. Буферизированный канал создается указанием размера буфера, в этом случае горутина не блокируется до тех пор, пока буфер не будет заполнен.
-
Какой размер у структуры
struct{}{}
?Пустая структура занимает 0 байт.
Ссылка: The empty struct
-
Есть ли в Go перегрузка методов или операторов?
Нет.
-
В какой последовательности будут выведены элементы
map[int]int
? Пример:m[0]=1 m[1]=124 m[2]=281
В случайной. Порядок вывода при итерации по мапе не гарантирован.
Ссылка: Go maps in action
-
В чем разница
make
иnew
?Функция
new(T)
выделяет «нулевую» память для нового элемента типаT
и возвращает его адрес, значение типа*T
. В терминологии Go он возвращает указатель на только что выделенное нулевое значение типаT
. Вот три различных способа создания указателяp
, указывающего на нулевое значениеbytes.Buffer
, каждый из которых эквивалентен:var buf bytes.Buffer p := &buf p := &bytes.Buffer{} p := new(bytes.Buffer)
Функция
make()
— это специальная встроенная функция, которая используется для инициализации слайсов, мап и каналов.make()
можно использовать только для инициализации слайсов, мап и каналов, и что, в отличие от функцииnew()
,make()
не возвращает указатель.Слайсы, мапы и каналы также можно инициализировать с помощью составных выражений. В качестве примеров ниже приведены два разных (но эквивалентных) способа инициализации мапы
m
:m := make(map[string]bool, 0) m := map[string]bool{}
Также можно инициализировать мапу и заполнить ее значениями:
m := map[string]bool{ "java": false, "go": true, }
Ссылка: new() vs make()
-
Сколько существует способов задать переменную типа
slice
илиmap
?// slice - 6 letters := []string{} letters := []string{"a", "b", "c", "d"} var letters []string letters := make([]string, 0) letters := make([]string, 0, 10) letters := new([]string) // map - 5 var m map[string]string m := map[string]string{ "123": "456", "test": "hello", } m := make(map[string]string) m := make(map[string]string, 10) m := new(map[string]string)
-
Что выведет данная программа и почему?
func update(p *int) { b := 2 p = &b } func main() { var ( a = 1 p = &a ) fmt.Println(*p) update(p) fmt.Println(*p) }
По-умолчанию в go аргументы в функцию передаются по значению. Если необходимо передать указатель, нужно это явно указать. Однако в go нет ссылок (невозможно создать две переменные с одним адресом). В примере выше, несмотря на то, что в функцию
update
передается указатель, переменная в главной функции не изменяется. p внутри функции update является локальной переменной, и изменения касаются только этой локальной переменной. Оригинальный указатель в main остается неизменным.Таким образом программа выведет:
1 1
Т.к. update принимает указатель и мы передаём &a, то нужно разыменовать его внутри функции - *p = b, а не передавать адрес p. Либо передавать в update двойной указатель -
update(p **int) *p = &b update(*p)
-
Что выведет данная программа и почему?
func main() { wg := sync.WaitGroup{} for i := 0; i < 5; i++ { wg.Add(1) go func(wg sync.WaitGroup, i int) { fmt.Println(i) wg.Done() }(wg, i) } wg.Wait() fmt.Println("exit") }
Будет дедлок. Так как мы передаем структуру
WaitGroup
в функцию по значению, она копируется, и вызовwg.Done()
внутри горутины не изменит счетчикWaitGroup
в главной функции. Следовательно, главная горутина навсегда зависнет наwg.Wait()
Для того, чтобы исправить эту ошибку, необходимо передавать в функцию структуру
WaitGroup
по указателю, либо не передавать его и использовать через замыкание. -
Что выведет данная программа и почему?
func main() { n := 0 if true { n := 1 n++ } fmt.Println(n) }
Программа выведет 0. Конструкция
if
создает новую область видимости, переменнаяn
создается в ней и инкрементируется. При этом переменнаяn
конструкцииif
не затрагивается, так как она находится в другой области видимости. -
Что выведет данная программа и почему?
func someAction(v []int8, b int8) { v[0] = 100 v = append(v, b) } func main() { var a = []int8{1, 2, 3, 4, 5} someAction(a, 6) fmt.Println(a) }
Программа выведет:
[100 2 3 4 5]
Слайс – это структура, которая содержит в себе длину, емкость и указатель на массив. Передавая слайс в функцию по значению, он копируется, но поля остаются теми же. Поэтому мы можем изменить значения элементов. При использовании append, т.к. вместимости слайса не хватает, то происходит выделение нового блока памяти для среза, и изменения касаются только этого нового среза, не влияя на оригинальный срез, который был передан в функцию.
-
Что выведет данная программа и почему?
func main() { slice := []string{"a", "a"} func(slice []string) { slice = append(slice, "a") slice[0] = "b" slice[1] = "b" fmt.Print(slice) }(slice) fmt.Print(slice) }
Программа выведет:
[b b a][a a]
В данной программе анонимной функции передается через параметры слайс, то есть он копируется. Меняя этот слайс мы не меняем слайс в главной функции.