"Правильно заданный вопрос - половина ответа". Осваиваю профессию Prompt Engineering. Это ответы на вопросы. Мопед не мой. Спасибо, Codeium. Не обрабатывал наводящие подвопросы, а надо бы. Но может быть кому-то пригодится и в таком виде.

Оглавление

  1. Go — императивный или декларативный? А в чём разница?

  2. Что такое type switch?

  3. Как сообщить компилятору, что наш тип реализует интерфейс?

  4. Как работает append?

  5. Какое у slice zero value? Какие операции над ним возможны?

  6. Как устроен тип map?

  7. Каков порядок перебора map?

  8. Что будет, если читать из закрытого канала?

  9. Что будет, если писать в закрытый канал?

  10. Как вы отсортируете массив структур по алфавиту по полю Name?

  11. Что такое сериализация? Зачем она нужна?

  12. Сколько времени в минутах займет у вас написание процедуры обращения односвязного списка?

  13. Где следует поместить описание интерфейса: в пакете с реализацией или в пакете, где этот интерфейс используется? Почему?

  14. Предположим, ваша функция должна возвращать детализированные Recoverable и Fatal ошибки. Как это реализовано в пакете net? Как это надо делать в современном Go?

  15. Главный недостаток стандартного логгера?

  16. Есть ли для Go хороший orm? Ответ обоснуйте.

  17. Какой у вас любимый линтер?

  18. Можно ли использовать один и тот же буфер []byte в нескольких горутинах?

  19. Какие типы мьютексов предоставляет stdlib?

  20. Что такое lock-free структуры данных, и есть ли в Go такие?

  21. Способы поиска проблем производительности на проде?

  22. Стандартный набор метрик prometheus в Go-программе?

  23. Как встроить стандартный профайлер в свое приложение?

  24. Overhead от стандартного профайлера?

  25. Почему встраивание — не наследование?

  26. Какие средства обобщенного программирования есть в Go?

  27. Какие технологические преимущества языка Go вы можете назвать?

  28. Какие технологические недостатки языка Go вы можете назвать?

1. Go — императивный или декларативный? А в чём разница?

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

Разница между императивным и декларативным программированием заключается в способе описания логики программы.

В императивном программировании разработчик указывает конкретные инструкции, как выполнить определенные действия. Программа последовательно выполняет эти инструкции и изменяет состояние программы. Примеры императивных языков программирования включают C, Java и Python.

В декларативном программировании разработчик описывает желаемый результат или состояние программы, а не конкретные шаги, которые нужно выполнить для достижения этого результата. Программа сама определяет, как достичь этого результата. Примеры декларативных языков программирования включают SQL, HTML и CSS.

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

2. Что такое type switch?

Type switch в Go - это конструкция языка, которая позволяет проверять тип значения и выполнять различные действия в зависимости от этого типа. Она представляет собой расширение обычного оператора switch, который используется для сопоставления значений с определенными случаями.

Вместо сопоставления значений с конкретными значениями, как в обычном операторе switch, type switch в Go позволяет сопоставлять значения с типами. Это особенно полезно, когда нужно выполнить разные действия в зависимости от типа значения.

Пример использования type switch в Go:

var x interface{} = 42

switch x.(type) {
case int:
    fmt.Println("Это целое число")
case string:
    fmt.Println("Это строка")
default:
    fmt.Println("Неизвестный тип")
}

В этом примере переменная x имеет тип interface{}, и мы используем type switch для определения типа значения x и выполнения соответствующих действий. В зависимости от типа значения x, будет выведено соответствующее сообщение.

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

3. Как сообщить компилятору, что наш тип реализует интерфейс?

В Go применяется "утиная типизация". Чтобы сообщить компилятору, что ваш тип реализует определенный интерфейс в Go, вам необходимо явно объявить это, указав имя интерфейса после имени типа в объявлении. Например:

type MyType struct {
    // поля вашего типа
}

// MyType реализует интерфейс MyInterface
func (m MyType) SomeMethod() {
    // реализация метода интерфейса
}

type MyInterface interface {
    SomeMethod()
}

В этом примере тип MyType реализует интерфейс MyInterface. Мы объявляем метод SomeMethod() в MyType, который соответствует сигнатуре метода в интерфейсе MyInterface. Таким образом, компилятору будет известно, что тип MyType реализует интерфейс MyInterface.

Вы также можете явно указать, что тип реализует интерфейс, путем использования пустого идентификатора (_), если вам не нужно использовать методы интерфейса внутри типа. Например:

type MyType struct {
    // поля вашего типа
}

// MyType реализует интерфейс MyInterface
var _ MyInterface = MyType{}

В этом случае мы используем пустой идентификатор (_), чтобы указать, что тип MyType реализует интерфейс MyInterface, но не используем методы интерфейса внутри типа.

4. Как работает append?

Функция append в Go используется для добавления элемента или элементов в конец среза (slice). Она имеет следующий синтаксис:

newSlice := append(slice, element1, element2, ...)

Где slice - исходный срез, к которому вы хотите добавить элементы, element1, element2, и т.д. - элементы, которые вы хотите добавить в конец среза, и newSlice - новый срез, содержащий все элементы из исходного среза и добавленные элементы.

Функция append возвращает новый срез, поскольку в Go срезы являются ссылочными типами, и функция append может изменять принадлежащую память.

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

slice1 := []int{1, 2, 3}
slice2 := []int{4, 5, 6}

newSlice := append(slice1, slice2...)

В этом примере мы добавляем элементы из slice2 в конец slice1 с использованием оператора ....

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

5. Какое у slice zero value? Какие операции над ним возможны?

Zero value для среза (slice) в Go является nil. Если вы объявляете срез без инициализации, его значение по умолчанию будет nil. Например:

var s []int // zero value для среза - nil

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

Операции, которые можно выполнить над срезом с нулевым значением nil, сводятся к проверке на нулевое значение или инициализации среза с помощью функции make. Например:

var s []int

if s == nil {
    fmt.Println("Срез равен nil")
}

s = make([]int, 0)

В этом примере мы проверяем, является ли срез s нулевым значением nil. Затем мы инициализируем срез s с помощью функции make, чтобы он стал действительным срезом с нулевой длиной и вместимостью.

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

6. Как устроен тип map?

Тип map в Go представляет собой ассоциативный массив или словарь, который состоит из набора пар ключ-значение. Ключи и значения могут быть любого типа, но все ключи должны быть одного типа, и все значения должны быть одного типа.

Внутри себя, map реализован как хэш-таблица, что обеспечивает эффективный доступ к значениям по ключу. Хэш-таблица является структурой данных, которая использует хэш-функцию для преобразования ключа в индекс внутреннего массива. Это позволяет быстро находить и обновлять значения по ключу.

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

Пример создания и использования map в Go:

m := make(map[string]int)

m["apple"] = 1
m["banana"] = 2
m["cherry"] = 3

fmt.Println(m["banana"]) // Выводит: 2

delete(m, "cherry")

fmt.Println(len(m)) // Выводит: 2

В этом примере мы создаем map с типом string в качестве ключа и int в качестве значения. Мы добавляем несколько элементов в map, обращаемся к значению по ключу, удаляем элемент по ключу и выводим длину map.

7. Каков порядок перебора map?

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

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

Пример сортировки ключей map перед итерацией:

m := map[string]int{
    "banana": 2,
    "apple": 1,
    "cherry": 3,
}

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}

sort.Strings(keys)

for _, k := range keys {
    fmt.Println(k, m[k])
}

В этом примере мы создаем map с неупорядоченными ключами и значениями. Затем мы извлекаем ключи из map в срез keys, сортируем этот срез с помощью sort.Strings, а затем итерируемся по отсортированным ключам, чтобы получить доступ к соответствующим значениям.

Важно отметить, что сортировка выполняется на ключах, а не на значениях map. Если вам нужно сортировать по значениям, вы можете использовать дополнительные структуры данных или преобразовывать map в список пар ключ-значение и сортировать его.

8. Что будет, если читать из закрытого канала?

ch := make(chan int, 5)
ch <- 1
ch <- 2
close(ch)

for {
    value, ok := <-ch
    if !ok {
        fmt.Println("Канал закрыт")
        break
    }
    fmt.Println(value)
}

// Ответ:
// 1
// 2
// Канал закрыт

В этом примере мы создаем буферизованный канал ch и добавляем в него два значения. Затем мы закрываем канал с помощью close(ch). В цикле мы пытаемся читать из канала и проверяем значение ok, чтобы определить, был ли канал закрыт или нет. Если канал закрыт, мы выходим из цикла.

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

9. Что будет, если писать в закрытый канал?

Если вы пытаетесь записать значение в закрытый канал в Go, это вызовет панику (panic). Попытка отправить значение в закрытый канал считается ошибкой выполнения и приводит к аварийному завершению программы.

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

Пример записи в закрытый канал:

ch := make(chan int, 5)
close(ch)

ch <- 1 // Это вызовет панику

В этом примере мы создаем буферизованный канал ch и затем закрываем его с помощью close(ch). После закрытия канала мы пытаемся записать значение 1 в канал, что вызывает панику.

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

Channel Axioms

- Отправка на нулевой канал блокируется навсегда

- Прием от нулевого канала блокируется навсегда

- Отправка в закрытый канал приводит к панике

- Прием из закрытого канала немедленно возвращает нулевое значение

10. Как вы отсортируете массив структур по алфавиту по полю Name?

Для сортировки массива структур по алфавиту по полю Name в Go, вы можете использовать интерфейс sort.Interface и функцию sort.Sort() из пакета sort.

Вот пример кода, который демонстрирует, как отсортировать массив структур по полю Name:

package main

import (
    "fmt"
    "sort"
)

type Person struct {
    Name string
    Age  int
}

type ByName []Person

func (a ByName) Len() int           { return len(a) }
func (a ByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a ByName) Less(i, j int) bool { return a[i].Name < a[j].Name }

func main() {
    people := []Person{
        {Name: "Alice", Age: 25},
        {Name: "Charlie", Age: 30},
        {Name: "Bob", Age: 20},
    }

    sort.Sort(ByName(people))

    for _, person := range people {
        fmt.Println(person.Name, person.Age)
    }
}

В этом примере мы определяем тип Person для представления структуры человека с полями Name и Age. Затем мы определяем тип ByName, который является срезом структур Person. Мы также реализуем методы Len(), Swap(), и Less() для типа ByName, чтобы он соответствовал интерфейсу sort.Interface.

Затем мы создаем срез структур Person и заполняем его некоторыми значениями. Далее мы вызываем sort.Sort(ByName(people)), чтобы отсортировать срез структур по полю Name. Наконец, мы проходимся по отсортированному срезу и выводим отсортированные значения.

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

11. Что такое сериализация? Зачем она нужна?

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

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

Некоторые основные причины использования сериализации включают:

  • Передача данных через сеть: Сериализация позволяет упаковать данные объекта в байтовый поток или другой формат, который может быть передан через сеть, например, по протоколу HTTP или TCP/IP.

  • Сохранение состояния объекта: Сериализация позволяет сохранить состояние объекта в файл или базу данных и восстановить его позже. Это полезно, например, для сохранения прогресса игры или хранения данных пользователя.

  • Взаимодействие между разными языками программирования: Сериализация позволяет передавать данные между программами, написанными на разных языках программирования. Объекты сериализуются в общий формат, который может быть понятен и восстановлен на другом языке.

  • Кэширование и копирование объектов: Сериализация может быть использована для создания копии объекта или для сохранения его состояния в кэше для повторного использования. Это может улучшить производительность и снизить нагрузку на систему.

Общий подход к сериализации включает выбор формата сериализации, определение, какие данные объекта должны быть сериализованы, и реализацию соответствующих методов (например, Serialize() и Deserialize()) для выполнения сериализации и десериализации объекта.

12. Сколько времени в минутах займет у вас написание процедуры обращения односвязного списка?

package main

import "fmt"

type Node struct {
	Val  int
	Next *Node
}

func reverseLinkedList(head *Node) *Node {
	var prev *Node
	curr := head
	for curr != nil {
		Next := curr.Next
		curr.Next = prev
		prev = curr
		curr = Next
	}
	return prev
}

// можно сократить curr
// func reverseLinkedList(head *Node) *Node {
// 	var prev *Node
// 	for head != nil {
// 		Next := head.Next
// 		head.Next = prev
// 		prev = head
// 		head = Next
// 	}
// 	return prev
// }

// можно сократить Next (через множественное присваивание)
// func reverseLinkedList(head *Node) *Node {
// 	var prev *Node
// 	for head != nil {
// 		head.Next, prev, head = prev, head, head.Next
// 	}
// 	return prev
// }

// или создавая новые узлы
// func reverseLinkedList(head *Node) *Node {
//   var result *Node
//   curr := head
//   for curr != nil {
//     result = &Node{curr.Val, result}
//     curr = curr.Next
//   }
//   return result
// }

func printLinkedList(head *Node) {
	current := head
	for current != nil {
		fmt.Printf("%d -> ", current.Val)
		current = current.Next
	}
	fmt.Println("nil")
}

func main() {
	node1 := &Node{Val: 1}
	node2 := &Node{Val: 2}
	node3 := &Node{Val: 3}
	node4 := &Node{Val: 4}
	node5 := &Node{Val: 5}
	node1.Next = node2
	node2.Next = node3
	node3.Next = node4
	node4.Next = node5
	fmt.Println("Исходный список:")
	printLinkedList(node1)
	reversedHead := reverseLinkedList(node1)
	fmt.Println("Обращенный список:")
	printLinkedList(reversedHead)
}

13. Где следует поместить описание интерфейса: в пакете с реализацией или в пакете, где этот интерфейс используется? Почему?

Описание интерфейса следует помещать в пакете, где этот интерфейс используется, а не в пакете с реализацией. Это согласуется с принципом разделения интерфейса и реализации (Interface Segregation Principle) из принципов SOLID.

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

Если описание интерфейса было бы помещено в пакете с реализацией, клиентам, которым нужно использовать этот интерфейс, пришлось бы иметь зависимость от пакета с реализацией. Это нарушает принцип разделения интерфейса и реализации и делает код менее гибким и сложным для поддержки и расширения.

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

14. Предположим, ваша функция должна возвращать детализированные Recoverable и Fatal ошибки. Как это реализовано в пакете net? Как это надо делать в современном Go?

В пакете net в стандартной библиотеке Go возвращение детализированных ошибок реализовано с помощью ошибок, которые являются значениями пользовательских типов, реализующих интерфейс net.Error. Этот интерфейс определен следующим образом:

type Error interface {
    error
    Timeout() bool   // возвращает true, если ошибка связана с таймаутом
    Temporary() bool // возвращает true, если ошибка является временной
}

Интерфейс net.Error включает методы Timeout() и Temporary(), которые позволяют клиентам проверять, является ли ошибка связанной с таймаутом или временной. Это позволяет клиентам принимать соответствующие действия в зависимости от типа ошибки.

В современном Go, начиная с версии 1.13, рекомендуется использовать синтаксис ошибок, основанный на типах, с помощью применения пакета errors и интерфейса fmt.Formatter. Этот подход позволяет создавать богатые по содержанию и информативные ошибки, которые могут хранить дополнительные данные и форматироваться в нужном формате при выводе.

Вот пример, как можно реализовать детализированные ошибки в современном Go:

package main

import (
    "errors"
    "fmt"
)

type RecoverableError struct {
    Message string
}

func (e *RecoverableError) Error() string {
    return fmt.Sprintf("RecoverableError: %s", e.Message)
}

type FatalError struct {
    Message string
}

func (e *FatalError) Error() string {
    return fmt.Sprintf("FatalError: %s", e.Message)
}

func main() {
    err := doSomething()

    switch {
    case errors.Is(err, &RecoverableError{}):
        fmt.Println("Recoverable error:", err)
    case errors.Is(err, &FatalError{}):
        fmt.Println("Fatal error:", err)
    default:
        fmt.Println("Unknown error:", err)
    }
}

func doSomething() error {
    // Ваша логика здесь

    return &RecoverableError{Message: "Something went wrong"}
}

В этом примере у нас есть два типа ошибок: RecoverableError и FatalError. Оба типа реализуют интерфейс error и предоставляют дополнительные данные в своих методах Error(). Функция doSomething() возвращает ошибку типа RecoverableError.

Функция main() использует функцию errors.Is() для проверки типа ошибки и выполняет соответствующие действия в зависимости от типа ошибки.

Важно отметить, что настройка и использование детализированных ошибок может различаться в зависимости от конкретной задачи и предпочтений разработчика. Рекомендуется следовать общим рекомендациям и принципам языка Go при работе с обработкой ошибок. Например, рекомендуется использовать типы ошибок, которые являются значениями пользовательских типов и реализуют интерфейс error, чтобы их можно было легко сравнивать с помощью функции errors.Is(), как показано в примере выше. Также рекомендуется предоставлять информативные сообщения об ошибках, чтобы облегчить отладку и понимание причины ошибки.

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

15. Главный недостаток стандартного логгера?

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

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

  • Ограниченная гибкость настройки: Стандартный логгер предоставляет ограниченные возможности для настройки формата вывода, уровня логирования и целевого места вывода. Например, он не поддерживает выбор формата вывода, смену цветов или выбор целевого файла журнала.

  • Отсутствие поддержки логирования в несколько мест назначения: Стандартный логгер не предоставляет встроенной поддержки для направления логов в несколько мест назначения. Например, если вы хотите отправить логи одновременно в консоль и в файл, вам придется самостоятельно реализовать эту функциональность.

  • Отсутствие поддержки ротации файлов журнала: Стандартный логгер не предоставляет встроенной поддержки для автоматической ротации файлов журнала. Если вы хотите сохранять логи в разных файлах или перезаписывать файл журнала после достижения определенного размера, вам придется самостоятельно реализовать эту функциональность.

  • Отсутствие поддержки уровней логирования: Стандартный логгер не предоставляет встроенной поддержки для уровней логирования, таких как отладка (debug), информация (info), предупреждение (warning) и т. д. Вам придется самостоятельно управлять уровнем логирования и фильтровать сообщения в соответствии с вашими потребностями.

В целом, стандартный логгер в Go является простым и удобным инструментом для базового логирования, но при необходимости расширенных функций и настроек, возможно, потребуется использовать сторонние библиотеки или реализовать собственный логгер для удовлетворения конкретных требований вашего проекта.

16. Есть ли для Go хороший orm? Ответ обоснуйте.

Да, для Go существует несколько хороших ORM (Object-Relational Mapping) библиотек, которые облегчают работу с базами данных и предоставляют удобный способ выполнения операций CRUD (Create, Read, Update, Delete) через объектно-ориентированный интерфейс. Некоторые из популярных ORM библиотек для Go включают:

  • GORM - это одна из самых популярных ORM библиотек для Go. Он предоставляет простой и удобный API для работы с базами данных, поддерживает различные драйверы баз данных, включая PostgreSQL, MySQL, SQLite и другие. GORM обладает широким функционалом, таким как автоматическое создание таблиц, миграции данных, поддержка отношений между таблицами и многое другое. Кроме того, GORM активно поддерживается сообществом разработчиков.

  • XORM - это еще одна популярная ORM библиотека для Go, которая предоставляет удобные функции для работы с базами данных. XORM поддерживает различные базы данных, включая MySQL, PostgreSQL, SQLite и другие. Библиотека предлагает гибкую систему отображения моделей на таблицы базы данных, поддержку транзакций, миграции данных и другие полезные функции.

  • Gorp - это относительно простая ORM библиотека для Go. Она предоставляет базовый набор функций для работы с базами данных, включая CRUD операции, отображение таблиц базы данных на структуры Go, поддержку транзакций и другие функции. Gorp поддерживает несколько баз данных, включая MySQL, PostgreSQL и SQLite.

Каждая из этих ORM библиотек имеет свои преимущества и недостатки, и выбор ORM зависит от конкретных требований и предпочтений разработчика. Однако, все они предоставляют удобные средства для работы с базами данных в Go и помогают упростить разработку приложений.

17. Какой у вас любимый линтер?

В сообществе Go разработчиков существует несколько популярных и широко используемых линтеров. Вот несколько из них:

  • GolangCI-Lint является мощным линтером для Go, предлагающим множество проверок кода на основе нескольких популярных линтеров, таких как Go Vet, GoLint, и других. Он предоставляет множество полезных проверок, которые помогают выявить потенциальные проблемы в коде и следовать принятым стандартам разработки Go.

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

  • Revive - это линтер для Go, который предлагает набор проверок и правил для анализа и улучшения качества кода. Он поддерживает настраиваемые правила и предоставляет детализированные отчеты об ошибках и предупреждениях. Revive также интегрируется с различными инструментами разработки и средами разработки, такими как VS Code и Goland.

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

18. Можно ли использовать один и тот же буфер []byte в нескольких горутинах?

В Go можно использовать один и тот же буфер []byte в нескольких горутинах, но это требует дополнительных мер предосторожности для обеспечения безопасности и избежания гонок данных (data races).

Одним из подходов к безопасному использованию одного буфера []byte в нескольких горутинах является синхронизация доступа к нему с помощью мьютексов или других механизмов синхронизации, таких как sync.RWMutex. Мьютексы позволяют горутинам синхронизировать свой доступ к общему ресурсу и предотвращать одновременную запись или чтение из буфера.

Вот пример использования мьютекса для безопасного доступа к общему буферу []byte:

var bufferMutex sync.Mutex
var buffer []byte

func writeToBuffer(data []byte) {
    bufferMutex.Lock()
    defer bufferMutex.Unlock()
    // Здесь происходит запись в буфер
    // ...
}

func readFromBuffer() []byte {
    bufferMutex.Lock()
    defer bufferMutex.Unlock()
    // Здесь происходит чтение из буфера
    // ...
    return buffer
}

В этом примере мьютекс bufferMutex используется для блокировки доступа к буферу []byte перед его записью или чтением. Это гарантирует, что только одна горутина имеет доступ к буферу в определенный момент времени.

Однако, важно помнить, что использование общего буфера []byte может быть проблематичным, особенно если горутины модифицируют его содержимое. Если горутины выполняют параллельные записи в буфер, могут возникнуть состояния гонки и непредсказуемые результаты. В таких случаях рекомендуется использовать другие механизмы синхронизации, такие как каналы (channels) или пулы буферов, чтобы гарантировать безопасность и предсказуемость работы с данными в многопоточной среде.

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

19. Какие типы мьютексов предоставляет stdlib?

Стандартная библиотека Go (stdlib) предоставляет два типа мьютексов для синхронизации доступа к общим ресурсам:

sync.Mutex: Это самый простой тип мьютекса, который предоставляется стандартной библиотекой Go. Он обеспечивает эксклюзивную блокировку (exclusive lock), что означает, что только одна горутина может захватить мьютекс и получить доступ к общему ресурсу. Если другая горутина пытается захватить мьютекс, пока он уже заблокирован, она будет ожидать его освобождения.

Пример использования sync.Mutex:

var mutex sync.Mutex
var sharedResource int

// Горутина 1
mutex.Lock()
sharedResource = 42
mutex.Unlock()

// Горутина 2
mutex.Lock()
fmt.Println(sharedResource)
mutex.Unlock()

sync.RWMutex: Этот тип мьютекса, называемый также мьютексом чтения/записи (read/write mutex), обеспечивает более гибкую блокировку. Он позволяет нескольким горутинам захватывать мьютекс только для чтения (shared lock), разрешая параллельный доступ к общему ресурсу для чтения. Однако, при записи (exclusive lock) мьютекс блокируется, и другие горутины должны ждать его освобождения.

Пример использования sync.RWMutex:

var rwMutex sync.RWMutex
var sharedResource int

// Горутина 1 для записи
rwMutex.Lock()
sharedResource = 42
rwMutex.Unlock()

// Горутина 2 для чтения
rwMutex.RLock()
fmt.Println(sharedResource)
rwMutex.RUnlock()

Оба типа мьютексов, sync.Mutex и sync.RWMutex, являются потокобезопасными и предоставляют механизмы для синхронизации доступа к общим ресурсам в многопоточной среде. Выбор между ними зависит от требований вашего кода: если вам нужна только эксклюзивная блокировка, используйте sync.Mutex, а если вам требуется поддержка одновременного чтения и блокировки записи, используйте sync.RWMutex.

20. Что такое lock-free структуры данных, и есть ли в Go такие?

Lock-free структуры данных - это структуры данных, которые разработаны таким образом, чтобы обеспечить потокобезопасность без использования блокировок (locks) или других механизмов синхронизации, которые могут приводить к блокировке (blocking). Вместо этого, они используют атомарные операции и другие конструкции языка, чтобы гарантировать безопасность доступа к данным в многопоточной среде без необходимости блокировки всего ресурса.

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

В Go нет встроенных lock-free структур данных в стандартной библиотеке (stdlib). Однако, благодаря мощным примитивам синхронизации и атомарным операциям, предоставляемым пакетом sync/atomic, можно создать свои собственные lock-free структуры данных в Go.

sync/atomic предоставляет набор функций для выполнения атомарных операций над переменными, таких как чтение и запись без блокировки. Эти функции могут быть использованы для создания lock-free структур данных, таких как атомарные счётчики, списки или хеши.

Создание собственных lock-free структур данных требует глубокого понимания атомарных операций и синхронизации в Go. Они могут быть сложными в реализации и подвержены проблемам, таким как ABA-проблема. Поэтому, при разработке lock-free структур данных, рекомендуется обратиться к установленным практикам и библиотекам, разработанным сообществом Go, чтобы избежать потенциальных проблем и ошибок.

В заключение, хотя Go не имеет предопределенных lock-free структур данных в стандартной библиотеке, можно создать собственные lock-free структуры данных, используя примитивы синхронизации и атомарные операции, предоставляемые пакетом sync/atomic.


sync.Map - это тип данных в пакете sync в Go, который представляет собой безопасную для параллельного доступа карту (map). Он обеспечивает безопасное чтение и запись данных в карту из нескольких горутин без необходимости явной синхронизации.

sync.Map является потокобезопасным и предоставляет методы для чтения и записи данных. Он использует внутренние механизмы для обеспечения безопасности, включая механизмы блокировки и сегментированную хэш-таблицу.

Однако, важно отметить, что sync.Map не является полностью lock-free. Внутри sync.Map используется блокировка для защиты от гонок данных, но блокировка не накладывается на всю карту целиком. Вместо этого, она использует сегментированную хэш-таблицу, где каждый сегмент имеет свою собственную блокировку. Это позволяет горутинам независимо выполнять операции чтения и записи в разные сегменты карты, минимизируя блокировку и повышая производительность.

Таким образом, хотя sync.Map обеспечивает безопасность параллельного доступа к картам, он все равно использует блокировки для обеспечения этой безопасности.

21. Способы поиска проблем производительности на проде?

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

  • Мониторинг ресурсов: Используйте инструменты мониторинга, такие как системные метрики, мониторинг нагрузки CPU, памяти, диска и сети. Это поможет вам определить, какие ресурсы используются в большей степени и могут быть узкими местами.

  • Профилирование кода: Используйте профилирование кода для идентификации узких мест в вашем коде. В Go вы можете использовать инструменты профилирования, такие как pprof и go tool pprof, чтобы получить информацию о времени выполнения и использовании памяти в вашей программе.

  • Логирование: Разместите подробные и информативные логи в вашем приложении. Логирование может помочь идентифицировать узкие места в вашем коде или обнаружить неожиданные события или проблемы использования ресурсов.

  • Измерение времени выполнения: Измерьте время выполнения различных частей вашего кода, чтобы определить, какие операции занимают больше всего времени. В Go вы можете использовать пакет time для измерения времени выполнения конкретных операций.

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

  • Масштабирование и горизонтальное масштабирование: Если ваше приложение испытывает проблемы с производительностью на проде, рассмотрите возможность масштабирования, как вертикального (увеличение ресурсов на одном сервере) и горизонтального (добавление дополнительных серверов). Это может помочь распределить нагрузку и повысить производительность.

  • Анализ кода и оптимизация: Просмотрите свой код и ищите возможности для оптимизации. Иногда простые изменения в алгоритмах или структурах данных могут привести к значительному улучшению производительности.

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

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

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


Смотри ещё: Рефакторинг программы на Go: ускорение в 23 раза

22. Стандартный набор метрик prometheus в Go-программе?

В Go-программе для сбора и экспорта метрик в Prometheus обычно используется пакет github.com/prometheus/client_golang/prometheus. Этот пакет предоставляет стандартный набор метрик и инструментов для работы с Prometheus.

Вот несколько основных типов метрик, которые можно использовать с помощью пакета prometheus:

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

counter := prometheus.NewCounter(prometheus.CounterOpts{
    Name: "my_counter",
    Help: "This is my counter",
})
counter.Inc()

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

gauge := prometheus.NewGauge(prometheus.GaugeOpts{
    Name: "my_gauge",
    Help: "This is my gauge",
})
gauge.Set(42)

Histogram: Гистограммы (Histogram) представляют собой метрики, которые измеряют распределение значений в заданном диапазоне. Они полезны для измерения времени выполнения операций или размера запросов. Пример:

histogram := prometheus.NewHistogram(prometheus.HistogramOpts{
    Name: "my_histogram",
    Help: "This is my histogram",
    Buckets: []float64{0.1, 0.5, 1, 2, 5},
})
histogram.Observe(0.6)

Summary: Суммары (Summary) также представляют собой метрики, которые измеряют распределение значений, но с учетом квантилей. Они полезны для измерения производительности и времени выполнения операций. Пример:

summary := prometheus.NewSummary(prometheus.SummaryOpts{
    Name: "my_summary",
    Help: "This is my summary",
})
summary.Observe(2.5)

Кроме того, пакет github.com/prometheus/client_golang/prometheus также предоставляет дополнительные инструменты для регистрации метрик (Register), создания коллекторов (Collector), экспорта метрик (HTTPHandler) и т.д.

Это лишь небольшой обзор стандартного набора метрик, доступных в Go-программе через пакет prometheus. Более подробную информацию о создании и использовании метрик в Prometheus вы можете найти в официальной документации Prometheus для Go-программы: https://pkg.go.dev/github.com/prometheus/client_golang/prometheus

Там вы найдете более подробную информацию о различных типах метрик, настройке и регистрации метрик, использовании коллекторов, экспорте метрик через HTTP и многое другое.

23. Как встроить стандартный профайлер в свое приложение?

В Go вы можете использовать встроенный профайлер для сбора информации о производительности вашего приложения. Для этого вам нужно импортировать пакет net/http/pprof и зарегистрировать его обработчики HTTP.

Вот простой пример того, как встроить стандартный профайлер в свое приложение:

package main

import (
    "log"
    "net/http"
    _ "net/http/pprof"
)

func main() {
    // Регистрируем обработчики профайлера
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // Ваше приложение

    // ...
}

В этом примере мы импортируем пакет net/http/pprof и регистрируем его обработчики HTTP с помощью функции http.ListenAndServe(). Обработчики будут доступны по адресу localhost:6060.

После запуска вашего приложения вы можете открыть веб-браузер и перейти по адресу http://localhost:6060/debug/pprof/, чтобы получить доступ к различным профилировочным эндпоинтам. Например:

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

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

24. Overhead от стандартного профайлера?

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

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

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

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

Если вы хотите более точно измерить overhead профайлера в вашем конкретном случае, вы можете использовать инструменты профилирования, такие как go tool pprof, чтобы анализировать профилировочные данные и определить, какое влияние профайлер оказывает на производительность вашего приложения.

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

25. Почему встраивание — не наследование?

В программировании принцип "встраивания" (composition) и "наследования" (inheritance) являются двумя основными подходами к организации и повторному использованию кода.

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

Наследование (inheritance), с другой стороны, подразумевает создание нового класса на основе существующего класса, называемого родительским классом или базовым классом. Новый класс, называемый дочерним классом или производным классом, наследует свойства и методы от родительского класса. Это позволяет дочернему классу наследовать и переопределять функциональность родительского класса.

Почему встраивание (composition) иногда предпочтительнее наследования (inheritance)? Вот несколько причин:

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

Повторное использование: Встраивание позволяет повторно использовать компоненты в более широком контексте. Вы можете использовать один и тот же компонент в разных объектах или классах, что приводит к повышению эффективности и уменьшению дублирования кода.

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

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

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

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

26. Какие средства обобщенного программирования есть в Go?

На данный момент, Go не поддерживает полноценное обобщенное программирование (generic programming) в том смысле, в каком оно реализовано в некоторых других языках, таких как C++ или Java.

Однако, начиная с версии Go 1.18, в Go был представлен новый механизм, называемый "Type Parameters", который вводит возможность определения обобщенных функций и типов данных. Этот механизм позволяет создавать функции и типы, которые могут работать с различными типами данных без необходимости явного создания дублированного кода.

Механизм "Type Parameters" в Go позволяет определять параметры типа с помощью ключевого слова type и использовать их в определении функций и структур. Например, вот как может выглядеть определение обобщенной функции PrintSlice, которая печатает содержимое среза любого типа:

func PrintSlice[T any](s []T) {
    for _, v := range s {
        fmt.Println(v)
    }
}

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

Хотя новый механизм "Type Parameters" в Go предоставляет некоторую возможность обобщенного программирования, он все еще ограничен по сравнению с полноценными системами обобщений, такими как в C++ или Java. В частности, в Go не поддерживается создание обобщенных структур данных, таких как контейнеры (например, обобщенные списки или деревья).

В целом, обобщенное программирование в Go все еще находится на начальном этапе развития, и механизм "Type Parameters" в Go 1.18 предоставляет некоторые возможности для работы с обобщенным кодом. Однако, если вам требуется более мощный и полноценный механизм обобщений, вам может потребоваться использовать другие языки программирования, которые поддерживают обобщенное программирование изначально.

27. Какие технологические преимущества языка Go вы можете назвать?

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

  • Простота и эффективность: Go разработан с упором на простоту, читаемость и эффективность. Он имеет небольшое количество ключевых слов и простую синтаксическую структуру, что делает его легким для изучения и использования. Компилятор Go также генерирует быстрый и эффективный машинный код, что делает его подходящим для создания высокопроизводительных приложений.

  • Конкурентность и параллелизм: Go включает в себя встроенную поддержку для работы с параллельными и конкурентными задачами. Он предоставляет механизмы горутин (goroutines) и каналов (channels), которые позволяют легко создавать и управлять множеством легковесных потоков выполнения. Это делает Go идеальным выбором для разработки сетевых и многопоточных приложений.

  • Быстрая компиляция: Компилятор Go обладает высокой скоростью компиляции. Благодаря этому, разработчики могут быстро получать обратную связь от компилятора и быстро запускать свои программы. Быстрая компиляция также делает Go подходящим для разработки микросервисов и других приложений, где важна быстрая итерация разработки.

  • Сборка мусора: Go имеет встроенную систему сборки мусора (garbage collection), которая автоматически управляет памятью и освобождает неиспользуемые объекты. Это позволяет разработчикам избежать ручного управления памятью и сосредоточиться на более высокоуровневых задачах.

  • Кросс-платформенность: Go поддерживает кросс-платформенную разработку, что позволяет писать код один раз и запускать его на различных операционных системах, таких как Windows, macOS и Linux. Это упрощает разработку и обеспечивает переносимость приложений.

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

  • Статическая типизация: Go является статически типизированным языком программирования, что означает, что типы переменных проверяются на этапе компиляции. Это помогает выявлять ошибки в коде на ранних стадиях разработки и повышает надежность и безопасность программ.

  • Низкий уровень абстракции: Go предоставляет возможность писать код на низком уровне абстракции, что позволяет разработчикам более точно контролировать поведение программы. Это особенно полезно для разработки системного программного обеспечения, сетевых приложений и других приложений, где требуется более прямое взаимодействие с аппаратным и программным обеспечением.

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

28. Какие технологические недостатки языка Go вы можете назвать?

Хотя язык программирования Go имеет множество преимуществ, есть и некоторые технологические недостатки, о которых следует упомянуть. Ниже приведены некоторые из них:

  • Отсутствие полноценной поддержки обобщенного программирования: До версии Go 1.18, Go не поддерживал полноценное обобщенное программирование (generic programming). Это означает, что вам может потребоваться повторять код для работы с разными типами данных, что может привести к дублированию кода и увеличению размера программы. Однако, с введением механизма "Type Parameters" в Go 1.18, эта проблема стала менее значимой.

  • Ограниченные возможности работы с исключениями: Go использует механизм ошибок (error) для обработки ошибок, вместо механизма исключений, который присутствует в некоторых других языках. Это может потребовать больше усилий со стороны разработчика при обработке ошибок и может привести к более громоздкому коду, особенно в сложных сценариях обработки ошибок.

  • Отсутствие некоторых продвинутых языковых конструкций: Go был разработан с упором на простоту и минимализм, и некоторые продвинутые языковые конструкции, такие как функциональное программирование или оператор перегрузки, отсутствуют в языке. Это может ограничить некоторые возможности разработки и потребовать более традиционных подходов к решению задач.

  • Менее развитая экосистема библиотек: В сравнении с некоторыми другими языками программирования, экосистема библиотек и инструментов Go может быть менее развитой. Хотя стандартная библиотека Go предлагает множество функциональности, некоторые специфические библиотеки или инструменты могут быть менее доступными или иметь меньшее количество альтернатив.

  • Ограниченная поддержка для некоторых архитектур и платформ: В настоящее время Go имеет ограниченную поддержку для некоторых архитектур и платформ, особенно в сравнении с более установленными языками программирования. Это может ограничить возможности разработки для некоторых специфических платформ и требовать дополнительных усилий для портирования кода на такие платформы.

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

Комментарии (35)


  1. lazy_val
    03.09.2023 20:05
    +6

    Мне одному кажется это это повтор вот этого?


    1. mrobespierre
      03.09.2023 20:05
      +3

      К Подольскому можно по-разному относиться как к человеку, но как 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, так что оверхед небольшой. Это - то зачем нужны каналы.


      1. VladimirFarshatov
        03.09.2023 20:05
        +1

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

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


        1. gudvinr
          03.09.2023 20:05
          +3

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


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

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


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


      1. comerc Автор
        03.09.2023 20:05


    1. lazy_val
      03.09.2023 20:05
      +1

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

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

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


      1. comerc Автор
        03.09.2023 20:05

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


  1. VladimirFarshatov
    03.09.2023 20:05
    +1

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

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

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

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

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

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

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

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


    1. VladimirFarshatov
      03.09.2023 20:05

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

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


      1. creker
        03.09.2023 20:05
        +1

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

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

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

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


        1. VladimirFarshatov
          03.09.2023 20:05
          +1

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

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

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

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

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


          1. creker
            03.09.2023 20:05

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

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

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


            1. VladimirFarshatov
              03.09.2023 20:05
              +1

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

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

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


            1. comerc Автор
              03.09.2023 20:05
              +1

              Всем известна простая истина - в 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.


              1. comerc Автор
                03.09.2023 20:05
                -1

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

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


      1. MihaTeam
        03.09.2023 20:05

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

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


        1. VladimirFarshatov
          03.09.2023 20:05
          +1

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

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

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

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


          1. creker
            03.09.2023 20:05
            -1

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

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


            1. VladimirFarshatov
              03.09.2023 20:05

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


              1. comerc Автор
                03.09.2023 20:05
                +1

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

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


    1. creker
      03.09.2023 20:05

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

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

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


      1. VladimirFarshatov
        03.09.2023 20:05

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


    1. comerc Автор
      03.09.2023 20:05
      -1

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

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

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


  1. creker
    03.09.2023 20:05

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


    1. MihaTeam
      03.09.2023 20:05
      +2

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


      1. creker
        03.09.2023 20:05
        +1

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

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

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


        1. MihaTeam
          03.09.2023 20:05
          +1

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


    1. comerc Автор
      03.09.2023 20:05

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


  1. iliazeus
    03.09.2023 20:05
    +3

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

    https://codeium.com/

    Codeium · Free AI Code Completion & Chat

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

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


    1. lazy_val
      03.09.2023 20:05

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


      1. DarkEld3r
        03.09.2023 20:05
        +1

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

        И зря.


      1. comerc Автор
        03.09.2023 20:05

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


  1. pda0
    03.09.2023 20:05
    +1

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

    *мем с гусём*

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

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

    :-D


  1. comerc Автор
    03.09.2023 20:05

    Добавил про Channel Axioms к 9-му вопросу.