Метод перехода: Указатель над типом приемника значения


Избегайте использования типа приемника значения в методах структур 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.

Надеюсь, эта статья может быть полезной и спасибо за прочтение

Оцените статью
devanswers.ru
Добавить комментарий