пятница, 30 октября 2020 г.

Go style guides: указатели на интерфейсы, проверка интерфейса

Указатели на интерфейсы

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

Интерфейс состоит из двух полей:

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

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

Проверить соответствие интерфейса

При необходимости проверьте соответствие интерфейса во время компиляции. Это включает в себя:

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

Плохой пример:

type Handler struct {
  // ...
}

func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}

Хороший пример:

type Handler struct {
  // ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

Утверждение var _ http.Handler = (*Handler)(nil) не будет скомпилировано, если *Handler когда-либо перестанет соответствовать интерфейсу http.Handler.

В правой части присваивания должно быть nil значение утвержденного типа. Это nil для типов указателей (например, *Handler), срезов и карт и пустая структура для типов структур.

type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}

var _ http.Handler = LogHandler{}

func (h LogHandler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}


Читайте также:


воскресенье, 4 октября 2020 г.

Срезы как аргументы в Golang

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

Первый пример. Срез передается функции в качестве аргумента, где он будет изменен.

https://play.golang.org/p/r5mKX5ErwLC

package main

import "fmt"

func change(abc []int) {
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4]
[4 4 4]

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

Второй пример.

https://play.golang.org/p/5ruLrp6ZJJc

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4]
[1 2 3]

Что изменилось? Чем этот пример отличается от первого? Мы просто добавляем один элемент в срез внутри функции. Но это существенно меняет срез. Создает новый срез. Как? Когда мы создаем срез вне функции, он создает базовый массив размером для 3 элементов. Затем мы передаем ссылку в функцию. Но когда мы хотим добавить еще один элемент, у нас нет места для него в базовом массиве. Затем встроенная функция добавления создает новый массив и новый срез. Когда мы меняем в нем значения, мы не меняем первый срез.

Третий пример.

https://play.golang.org/p/0tKWomkDCk3

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 5)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    change(abc)
    abc = append(abc, 4)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4]
[1 2 3 4]

Почему, когда мы меняем срез вне функции, он не будет указывать на измененный срез в функции? Ну это просто совершенно разные срезы - и все. Когда мы используем append вне функции, он создает для него новый массив и новый срез.

Пример четвертый.

https://play.golang.org/p/uCDG59fJLFm

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    abc = append(abc, 4)
    change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4 4]
[4 4 4 4]

Почему этот пример отличается от третьего? Когда мы добавляем срез перед передачей его функции, он создает базовый массив с емкостью не только для одного добавленного элемента, но и с начальным числом элементов, кратным 1,5 - для 6 элементов. Мы можем это увидеть:

package main

import "fmt"

func change(abc []int) {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
}

func main() {
    abc := []int{1, 2, 3}
    abc = append(abc, 4)
    fmt.Println(cap(abc))
    change(abc)
    fmt.Println(abc)
}

Вывод:

6
[4 4 4 4 4]
[4 4 4 4]

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

Эти примеры могут привести к неожиданным для кого-то результатам. Как этого избежать?

Просто верните срез из функции. Он вернет ссылку на новый срез, если он был создан.

package main

import "fmt"

func change(abc []int) []int {
    abc = append(abc, 4)
    for i := range abc {
        abc[i] = 4
    }
    fmt.Println(abc)
    return abc
}

func main() {
    abc := []int{1, 2, 3}
    abc = change(abc)
    fmt.Println(abc)
}

Вывод:

[4 4 4 4]
[4 4 4 4]

И напишите модульные тесты. Скорее всего, они обнаружат неожиданное поведение. Если применимо, сначала напишите тесты - используйте Test Driven Development.


Читайте также: