Избегайте использования типа приемника значения в методах структур Golang
Об этой статье
Эта статья предполагает, что читатели уже имеют базовые знания о struct
, methods
и goroutine
в языке программирования Go.
Основная цель этой статьи — напомнить себе об использовании типа приемника value/pointer в методах struct (язык программирования Go).
Я решил написать об этом, несмотря на то, что это очень простая вещь,
после того, как я потратил более 1 часа на отладку проблемы, вызванной использованием типа приемника значения в методах struct.
В примере ниже я покажу проблему, вызванную использованием приемника значений, но без предупреждений.
Типы приемников в методах структур
В Golang мы определяем методы для struct аналогично методам в объектно-ориентированном языке программирования.
Разница с языком OO в том, что в Golang есть два типа приемников: value
и pointer
.
Тип приемника указателя копирует указатель на «объект/структуру» в памяти. Это то же самое, что и методы в ОО языке программирования, где любые изменения, сделанные в методах, будут изменять фактический «объект/структуру» в памяти.
Пример типа указателя-приемника приведен ниже:
// Define a structtype Book struct {
Title string
}
// A pointer receiver type method.
// Marked with the asterisk (*) sign.
func(b *Book) ChangeTitle(title string) {
b.Title = title
}
Тип приемника значения будет копировать данные ‘объекта/структуры’. Любые изменения, внесенные в объект/структуру в методах, не изменят фактический объект/структуру в памяти.
Пример типа приемника значений приведен ниже:
// Value receiver type.
// Marked with no asterisk (*) sign, in the struct receiver.
func (b Book) ChangeTitle(title string) {
b.Title = title
}
В Golang мы используем приемник значений только в том случае, если методу не нужно вносить какие-либо изменения в данные struct.
Однако это верно не для всех случаев. В этой статье я покажу пример, когда нам нужно использовать приемник указателя в методе «только для чтения».
Значение Тип указателя
Пример неэффективного присваивания
Предупреждение
package main
import (
"fmt"
)
type book struct {
title string
}
// Change the title of a book
func (b book) changeTitle(title string) {
b.title = title
}
type library struct {
books []book
}
func (lib library) addABook(book book) {
lib.books = append(lib.books, book)
}
func main() {
lib := library{}
fmt.Println(lib)
b := book{title: "Three little pigs"}
b.changeTitle("Blue Birds")
lib.addABook(b)
fmt.Println(lib)
}
Сервер языка Go (расширение vscode), gopls
, покажет предупреждение ineffective-assignment
для присваивания в методах changeTitle
и addABook
.
Это очень полезно для предотвращения ошибок в случае, если мы забыли использовать тип приемника указателя.
Пример в реализации goroutine
.
Существует и другая ситуация, когда необходим приемник указателя, но используется приемник значения, и при этом никаких предупреждений не выдается.
Такая ситуация возникает при использовании goroutine
, как в приведенном ниже коде:
import (
"fmt"
"time"
)
type book struct {
title string
}
type library struct {
books []book
}
func (lib *library) addABook(book book) {
fmt.Printf("Add book '%s'n", book.title)
lib.books = append(lib.books, book)
}
// Method below is mimicking time consuming operation.
// Method below is a Value receiver type.
func (lib library) timeConsumingOperation() {
// `Sleep` to mimick ` time-consuming operation.
time.Sleep(5 * time.Millisecond)
// Check the number of books
fmt.Printf("Value receiver type. Found %d books.n", len(lib.books))
}
func main() {
lib := library{}
// Goroutine mimicking a time consuming operation.
go lib.timeConsumingOperation()
// Add 2 books
go lib.addABook(book{title: "Rain Rain Go Away"})
go lib.addABook(book{title: "Rainbow After Rain"})
// `Sleep` to make sure all goroutines are completed
time.Sleep(2 * time.Second)
fmt.Printf("Actual number of books : %d books.n", len(lib.books))
}
Выполнение приведенного выше кода приведет к ложному результату.
Библиотека Library
в timeConsumingOperation
будет иметь устаревшую версию
(поскольку методы были запущены как goroutine
).
Add book 'Rainbow After Rain'
Add book 'Rain Rain Go Away'
Value receiver type. Found 0 books.
Actual number of books : 2 books.
Тип приемника указателя
Использование типа приемника указателя в методах struct является обязательным, если мы хотим добиться целостности данных.
Конечно, существует меньшее количество случаев, когда необходимо использовать тип приемника значения.
Модификация предыдущего примера с использованием типа приемника указателя в timeConsumingOperation
приведена ниже (добавлена звездочка(*)).
func (lib *library) timeConsumingOperation() {
Использование указателя в timeConsumingOperation
будет
указывает lib
на одни и те же данные в памяти, гарантируя целостность данных.
Выполнение модифицированного кода приведет к появлению :
Add book 'Rainbow After Rain'
Add book 'Rain Rain Go Away'
Value receiver type. Found 2 books.
Actual number of books : 2 books.
Заключение
Использование приемника указателей в методах, вместо приемников значений, обеспечит целостность данных.
Я считаю, что мы должны, возможно, за исключением некоторых случаев, всегда использовать типы приемников указателей, чтобы избежать потенциальных ошибок.
которые не отлавливаются инструментами, как в приведенном выше случае.
Вот ссылка на репозиторий Code xample в Github.com.
Надеюсь, эта статья может быть полезной и спасибо за прочтение