Dmitriy (r3code) wrote,
Dmitriy
r3code

Category:

7 незаметных подводных камней в языке Go

1. Размеры Int могут меняться в зависимости от разрядности. Так что ваш код может перестать работать на 32 разрядной машине, банально вызывая переполнение.

Эту ошибку можно увидеть в построителе парсеров языка antlr4 - разработчики просто не учли, что люди пользуются еще 32-разрядными системами, в итоге код на 32-разрядной ОС не работает из-за меншего размера Int вызывая переполнение.

2. Осторожнее с приведением типов к типам указателям. Указатель на тип — это не сам тип, тут не будет разыменовывание работать автоматически.

Пример:
f { ... return nil, NotFoundError } err = f()
if _, ok := err.(*NotFoundError); ok { // ... } // это не сработает!

Используйте лучше подход стандартной библиотеки GO — проверка ошибок не по типу, а по назначению, см. os.IsExist(error) и os.IsPermission(error).


3. Реализация интерфейсов

Описание ресиверов в реализации должно совпадать с объявлением в интерфейсе.

Пример: Foo и Bar используют указатели в ресиверах для реализации интерфейса fmt.Stringer:

type Foo struct{}

func (Foo) String() string { return "I am a Foo" }
type Bar struct{}
func (*Bar) String() string { return "I am a *Bar" }
var _ fmt.Stringer = Foo{}
var _ fmt.Stringer = &Foo{}
var _ fmt.Stringer = Bar{}
var _ fmt.Stringer = &Bar{}

Попытка присвоить Bar не сработает и компилятор об этом предупредит:

./test.go:10:5: cannot use Bar literal (type Bar) as type fmt.Stringer in assignment:   
Bar does not implement fmt.Stringer (String method has pointer receiver)

Попытка присвоить &Foo как fmt.Stringer сработает, т.к. GO автоматически разыменовывает указатели.

Если вы будете хранить реализацию в interface{} и пробуете использовать его как fmt.Stringer с приведением типов, то компилятор вам не подскажет, т.к. он не знает, что будет храниться в interface{}. Вы получите не то что ожидали. Пример:

fmt.Println(Foo{})
fmt.Println(&Foo{})
fmt.Println(Bar{})
fmt.Println(&Bar{})
// ** выведет  **
I am a Foo
I am a Foo
{} // вот он неожиданный результат
I am a *Bar

4. Тихое копирование мютексов

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

В коде ниже есть ошибка, можете найти?



type Thing struct {
Map map[string]string
mu sync.Mutex
}

func (t Thing) Read() {
t.mu.Lock()
defer t.mu.Unlock()
for k, v := range t.Map {
fmt.Println("read", k, v)
}
}

func (t *Thing) Write() {
t.mu.Lock()
defer t.mu.Unlock()
t.Map["entry"] = fmt.Sprintf("%v", time.Now())
}

func main() {
t := Thing{Map: map[string]string{}}
go func() {
for {
t.Read()
}
}()
for {
t.Write()
}
}



Виновник тут Read() - у которого вместо ресивера с указателем, ресивер по значению. Это в методе Read() при обращении к t.mu.Lock приводит к создании копии t и копии mu (мютекс), и это приводит к состоянию гонки (race condition). На моей машине этот код ничего не дает на вывод, как некоторые могли бы ожидать. Я подозреваю, это потому что: мютекс копируется в блокированном состоянии, читающая процедура никогда не увидит изменение, т.е. состояние может быть кешировано из-за отсутствия правильной синхронизации.

Как исправить?
Использовать ресивер с указателем в методах структур участвующих к кокурентой работе (используются в горутинах).

5. Сравнение указателей может давать не то, что вы ожидаете


Если вы сравниваете 2 указателя на строки a == b, то даже если строки одинаковы, вы можете получить false. При отладке это тяжело обнаружить, т.к. в выражении нет четкого признака, который показал бы вам, что в этот момент работа идет с указателями. Вам придется просматривать объявления типов, что может оказаться еще сложении из-за автоматического определения типов (type inference).

6. Повторное использование переменных ошибки (error var)


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

var err error

go func() {
if err = thing(); err != nil {
// ...
}
}

if err = otherThing(); err != nil {
// ...
}


Как исправить?
Создавайте уникальные переменные для ошибок, избегайте повторного использования.
Используйте детектор состояния гонки встроенный в Go, флаг -race при компиляции и тестировании - он позволяет определить такие ошибки.

7. Получение из закрытого канала


Прием из закрытого канала может быть сразу успешным при возврате нулевого значения для типа канала.
Но как вы собираетесь обрабатывать это значение? Это что-то значит для вашей программы? Это вызовет что-то плохое? Если вы получаете в цикле, ваш цикл может даже никогда не завершиться. Вы должны знать, когда вам нужно остановить обработку.
Как исправить?
Вы можете проверить, является ли значение, полученное вами от канала, нулевым, потому что оно было отправлено таким образом или возникло при закрытии канала (close). Используйте форму v, ok для проверки:

value, ok := <-someChannel
if !ok {
// ...
}


Когда `ok` ложно, канал быз закрыт и вы можете безопасно завершить работу, не оставляя что-либо в буфере канала.
Этот прием особенно полезен, когда вы не можете использовать цикл `range` для обхода канала, т.к. вам нужно делать `select` из других каналов.
Tags: golang, прочитано, работа, статья
Subscribe

Recent Posts from This Journal

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 0 comments