Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Работа с данными
Отличие make и new
Make и new – это встроенные механизмы для выделения памяти. Они используются в разных ситуациях и имеют свои особенности.
- new инициализирует нулевое значение для данного типа и возвращает указатель на этот тип.
- make используется исключительно для создания и инициализации срезов, отображений и каналов, возвращает ненулевой экземпляр указанного типа.
- Основное отличие между ними состоит в том, что make возвращает инициализированный тип, готовый к использованию после создания, а new – указатель на тип с его нулевым значением.
a := new(chan int) // a имеет тип *chan int
b := make(chan int) // b имеет тип chan int
Скрытые данные в слайсах
Слайс — это массив переменной длины, который может хранить элементы одного типа. Внутренне представляет собой ссылку на базовый массив.
При работе со слайсами часто возникает задача их «перенарезки» на более мелкие. В итоге получившийся слайс будет ссылаться на массив исходного. Об этом не стоит забывать, иначе в программе может возникнуть непредсказуемое потребление памяти.
Рассмотрим эту особенность на конкретных примерах:
// Плохая практика - непредсказуемое потребление памяти
func cutSlice() []byte {
slice := make([]byte, 256)
fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 <0x...>
return slice[:10]
}
func main() {
res := cutSlice()
fmt.Println(len(res), cap(res), &res[0]) // 10 256 <0x...>
}
Для предотвращения возникшей ошибки следует удостовериться, что копирование производится из временного слайса:
// Хорошая практика - данные скопированы из временного слайса
func cutSlice() []byte {
slice := make([]byte, 256)
fmt.Println(len(slice), cap(slice), &slice[0]) // 256 256 <0x...>
copyOfSlice := make([]byte, 10)
copy(copyOfSlice, slice[:10])
return copyOfSlice
}
func main() {
res := cutSlice()
fmt.Println(len(res), cap(res), &res[0]) // 10 256 <0x...>
}
Функции
Функции с множественным возвратом
Функции в языке Go могут возвращать несколько значений. Это называется «множественным возвратом». Данная особенность языка позволяет возвращать не только результат, но и дополнительные значения, такие как ошибки или другие необходимые данные.
Пример объявления функции с множественным возвратом в Go:
package main
import "fmt"
func swap(a, b int) (int, int) {
return b, a
}
func main() {
x, y := swap(1, 2)
fmt.Println(x, y) // 2 1
a, _ := swap(3, 4)
fmt.Println(a) // 4
}
В приведенном примере функция swap
принимает два аргумента типа int
и возвращает два значения того же типа, меняя местами исходные переменные.
Можно также игнорировать одно или несколько возвращаемых значений, используя пустой идентификатор (_
).
Функции с множественным возвратом особенно полезны, когда требуется возвращать несколько результатов, например, при работе с ошибками или при параллельной обработке данных.
Приведенная ниже функция openFile
возвращает два значения, одно из которых – ошибка или nil
в случае ее отсутствия.
func openFile(name string) (*File, error) {
file, err := os.Open(name)
if err != nil {
return nil, err
}
return file, nil
}
Интерфейсы
В Go интерфейсы представляют собой набор методов, определяющих поведение объекта. Они позволяют абстрагироваться от конкретной реализации и работать с различными типами данных. То есть интерфейсы лишь определяют некоторый функционал, но сами его не реализуют.
Используем интерфейсы правильно
package worker // worker.go
type Worker interface { Work() bool }
func Foo(w Worker) string { ... }
package worker // worker_test.go
type secondWorker struct{ ... }
func (w secondWorker) Work() bool { ... }
...
if Foo(secondWorker{ ... }) == "value" { ... }
Ниже представлен пример неправильного подхода при работе с интерфейсами:
// Плохая практика
package employer
type Worker interface { Worker() bool }
type defaultWorker struct{ ... }
func (t defaultWorker) Work() bool { ... }
func NewWorker() Worker { return defaultWorker{ ... } }
Верное решение с точки зрения Go — вернуть конкретный тип и позволить Worker
имитировать реализацию employer
:
// Хорошая практика
package employer
type Worker struct { ... }
func (w Worker) Work() bool { ... }
func NewWorker() Worker {
return Worker{
...
}
}
Конкурентность и параллелизм
Отслеживание горутин
Горутины дешевы в запуске и эксплуатации, но у них есть конечная стоимость с точки зрения занимаемой памяти – вы не можете создать их бесконечное количество. В отличие от переменных, среда выполнения Go не может обнаружить, что горутина больше никогда не будет использоваться.
- Подробно горутины рассмотрены в статье «Горутины: что такое и как работают». Её прочтение поможет лучше разобраться в рассматриваемой теме.
go
в своей программе для запуска горутины, вы должны знать, как и когда она завершится.Если вы не знаете ответа на два приведённых вопроса, это может привести к возникновению утечек памяти.
Обратимся к примеру для иллюстрации данной ошибки:
func leakGoroutine() {
ch := make(chan int)
go func() {
received := <- ch
fmt.Println("Полученное значение:", received)
}
}
Здесь функция leakGoroutine
запускает горутину, которая блокирует чтение из канала ch
. В результате в него ничего не отправится, и сам он никогда не закроется. Горутина будет заблокирована навсегда, вызов функции fmt.Println
никогда не произойдет.
Обнаружение утечек
Инженеры из Uber, которые принимают активное участие в развитии Go, создали детектор утечек горутин – пакет goleak, нацеленный на интеграцию с модульными тестами. Рассмотрим пример работы с этим инструментом на практике.
Пусть есть некая функция leakGoroutin
с утечкой горутины:
func leakGoroutine() {
go func() {
time.Sleep(time.Minute)
}()
return nil
}
И тест этой функции:
func TestLeakGoroutine(t *Testing.T) {
defer goleak.VerifyNone(t)
if err := leak(); err != nil {
t.Fatal("Fatal message")
}
}
При запуске тестов появляется сообщение об ошибке found enexpected goroutines
, где указывается вершина стека с проблемной горутиной, ее состояние и идентификатор.
Этот инструмент может быть полезен при создании программ, так как позволяет сократить время на нахождение и устранение утечек памяти.
Обработка ошибок и восстановление
Ошибки в Go представлены интерфейсом error, который определяет метод Error() string
. Любой тип, реализующий этот метод, может быть использован как ошибка.
type error interface {
Error() string
}
- Чтобы больше узнать об ошибках в Go, рекомендуется прочитать статью «Исключения в Go – это легко?». Из нее вы узнаете о том, как эффективно решать проблемные ситуации в программах.
Обрабатываем ошибки правильно
Игнорирование ошибок может привести к неопределенному поведению и усложнить отладку кода. Рассмотрим правильный способ обработки ошибок на примере работы с файлом:
// плохо
file, err := os.Open("filename.txt")
if err == nil {
// операции с файлом
}
// хорошо
file, err := os.Open("filename.txt")
if err != nil {
log.Fatal(err) // обработка ошибки
}
defer f.Close() // отложенный вызов функции для закрытия файла
Без паники, но с восстановлением
Классический способ сообщить об ошибке – вернуть тип error
. Но что делать в тех случаях, когда её нельзя быстро восстановить? Тогда на помощь приходит встроенная функция panic
(часто её называют просто «паника»), которая завершает программу и выводит настраиваемое сообщение об ошибке.
Ниже представлен пример простой функции с паникой:
package main
import "fmt"
func examplePanic() {
panic("Паника - программа завершена")
fmt.Println("Функция examplePanic успешно завершилась")
}
func main() {
examplePanic()
fmt.Println("Функция main успешно завершилась")
}
При возникновении паники функция завершается и происходит запуск оставшихся отложенных функций с помощью defer, а также раскручивание стека горутин. В реальных условиях разработки следует избегать подобных ситуаций, так как это ставит под угрозу бесперебойную работу программы. К счастью, авторы Go предусмотрели этот недостаток и создали механизм восстановления после паники – recover
. Он позволяет остановить раскручивание стека и вернуть разработчику контроль над программой.
Чтобы продемонстрировать работу данного механизма, обратимся к примеру:
package main
import "fmt"
func Recovery() {
if recoveryResult := recover(); recoveryResult != nil {
fmt.Println(recoveryResult)
}
fmt.Println("Восстановление...")
}
func Panic() {
defer Recovery()
panic("Паника")
fmt.Println("Функция Panic успешно завершилась")
}
func main() {
Panic()
fmt.Println("Функция main успешно завершилась")
}
В результате выполнения кода мы получим следующий вывод:
Паника
Восстановление...
Функция main успешно завершилась
Заметьте, что функция Panic
не завершается после паники. Это происходит из-за того, что с помощью defer вызывается отложенная функция Recovery
, которая восстанавливает работу программы. Далее исполнение передается в main
, где происходит успешное завершение всего кода.
Заключение
Важно помнить, что качество и чистота кода зависят не только от языка программирования, но и от навыков разработчика. Использование рассмотренных примеров и следование общим принципам помогут улучшить качество создаваемого программного обеспечения.
Хочется верить, что статья вдохновит читателей применять описанные практики в разработке на Go и создавать программы, в которых будет нетрудно разобраться даже новичку. И помните, чистый код – это путь к успешному проекту!
Комментарии