Программирование на языке Go: полезные советы. Часть 2

Продолжаем публикацию полезных советов по мотивам статьи «Go Tips 101». При использовании данных рекомендаций учитывайте, что далеко не все из них можно применять в production. Первая часть советов находится здесь.

1. Как определить наличие метода у значения, не прибегая к использованию пакета reflect?

Решение показано в следующем примере (здесь мы проверяем наличие метода, соответствующего прототипу M(int) string).

package main

import "fmt"

type A int
type B int
func (b B) M(x int) string {
    return fmt.Sprint(b, ": ", x)
}

func check(v interface{}) bool {
    _, has := v.(interface{M(int) string})
    return has
}

func main() {
    var a A = 123
    var b B = 789
    fmt.Println(check(a)) // false
    fmt.Println(check(b)) // true
}

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

Допустим, что некий пакет реализует функцию func NewX(...Option) *X, которая объединяет входные параметры с некоторыми внутренними параметрами, заданными по умолчанию. Приведенный ниже код для этого использовать не рекомендуется:

func NewX(opts ...Option) *X {
    options := append(opts, defaultOpts...)
    // Значение opts после слияния используется для формирования возвращаемого значения X.
    // ...
}

Потенциальная проблема данного кода заключается в том, что вызов функции append может изменить исходную последовательность opts типа Option, переданную в качестве аргумента. В большинстве случаев данная проблема не является критичной. Однако в некоторых особых сценариях это может привести к непредсказуемым результатам.

Ниже приведен код, который не вносит изменений в исходную последовательность элементов типа Option из входного аргумента:

func NewX(opts ...Option) *X {
    opts = append(opts[:len(opts):len(opts)], defaultOpts...)
    // Значение opts после слияния используется для формирования возвращаемого значения X.
    // ...
}

Однако если мы вызываем функцию NewX, то у нас нет никакой гарантии, что она не изменяет исходных элементов своего параметра-среза. Поэтому наилучшим решением является передача в функцию NewX входного параметра в виде трехаргументного среза.

Другой сценарий, в котором нужно использовать трехаргументный срез, описан в этой wiki-статье.

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

3. Как проверить и показать в коде реализацию пользовательским типом нужного интерфейса?

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

package myreader

import "io"

type MyReader uint16

func NewMyReader() *MyReader {
    var mr MyReader
    return &mr
}

func (mr *MyReader) Read(data []byte) (int, error) {
    switch len(data) {
    default:
        *mr = MyReader(data[0]) << 8 | MyReader(data[1])
        return 2, nil
    case 2:
        *mr = MyReader(data[0]) << 8 | MyReader(data[1])
    case 1:
        *mr = MyReader(data[0])
    case 0:
    }
    return len(data), io.EOF
}

// Каждая из трех строк ниже подтверждает,
// что тип *MyReader реализует интерфейс io.Reader.
var _ io.Reader = NewMyReader()
var _ io.Reader = (*MyReader)(nil)
func _() {_ = io.Reader(nil).(*MyReader)}

4. Некоторые полезные приемы проверки условий на этапе компиляции.

Помимо примера выше, существуют и другие способы проверки тех или иных условий на этапе компиляции.

Вот несколько примеров, которые будут корректно компилироваться, только если значение константы N не меньше значения константы M:

// Успешная компиляция каждой из приведенных ниже строк кода гарантирует, что N >= M
func _(x []int) {_ = x[N-M]}
func _(){_ = []int{N-M: 0}}
func _([N-M]int){}
var _ [N-M]int
const _ uint = N-M
type _ [N-M]int

// При условии, что M и N целые положительные числа.
var _ uint = N/M - 1

Ещё один прием, заимствованный у Люка Шампена (Luke Champine, @lukechampine в Твиттере). Данный метод основан на правиле, гласящем, что составной литерал не может содержать дублирующиеся ключи-константы.

var _ = map[bool]struct{}{false: struct{}{}, N>=M: struct{}{}}

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

var _ = map[bool]int{false: 0, N>=M: 1}

Аналогично можно проверять на этапе компиляции равенство значений двух констант:

var _ [N-M]int; var _ [M-N]int
type _ [N-M]int; type _ [M-N]int
const _, _ uint = N-M, M-N
func _([N-M]int, [M-N]int) {}

var _ = map[bool]int{false: 0, M==N: 1}

var _ = [1]int{M-N: 0} // the only valid index is 0
var _ = [1]int{}[M-N]  // the only valid index is 0

var _ [N-M]int = [M-N]int{}

Последняя строка кода также навеяна публикациями Люка Шампена.

Как проверить, что строковая константа является непустой:

type _ [len(aStringConstant)-1]int
var _ = map[bool]int{false: 0, aStringConstant != "": 1}
var _ = aStringConstant[:1]
var _ = aStringConstant[0]
const _ = 1/len(aStringConstant)

Последняя строка кода написана по мотивам изящной идеи Яна Меркла (Jan Mercl).

Иногда, чтобы не тратить лишнюю память на переменные, объявляемые на уровне пакета, можно поместить проверочный код в функцию с пустым идентификатором.

Пример:

func _() {
    var _ = map[bool]int{false: 0, N>=M: 1}
    var _ [N-M]int
}