Практика команды Go по обеспечению совместимости модулей Go

Go

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


Модули Go будут продолжать меняться с течением времени по мере добавления новых функций или рефакторинга некоторых общих частей модулей Go.

Однако выпуск новой версии Go Module — плохая новость для пользователей. Они должны найти новые версии, изучить новые API и изменить свой код. А некоторые пользователи могут никогда не обновляться, а это значит, что вам придется постоянно поддерживать две версии вашего кода. Поэтому обычно лучше изменить существующие модули Go совместимым образом.

В этой статье мы рассмотрим некоторые приемы кодирования, которые позволят вам поддерживать совместимость модуля Go. Основная идея такова: добавляйте, но не меняйте и не удаляйте код модуля Go. Мы также обсудим, как разработать высокосовместимый API с точки зрения макросов.

новая функция

В общем, изменение параметров функции — самый распространенный случай нарушения совместимости кода. Мы обсудили несколько способов решения этой проблемы, но давайте сначала рассмотрим неудачную практику.

Есть такая функция:

func Run(name string)

Когда мы хотим расширить эту функцию для определенной ситуации, добавим в эту функцию формальный параметрsize:

func Run(name string, size ...int)

Если вы находитесь в другом коде или пользователь модуля Go обновлен, тогда код, подобный следующему, будет иметь проблемы:

package mypkg
var runner func(string) = yourpkg.Run

оригинальныйRunТип функцииfunc(string), но новыйRunТип функции становитсяfunc(string, ...int), поэтому на этапе компиляции будет сообщено об ошибке. Вызывающий метод должен быть изменен в соответствии с новым типом функции, что доставляет массу неудобств и даже ошибок разработчикам, использующим Go Modules.

В этом случае вместо изменения сигнатуры функции мы можем добавить функцию для решения проблемы. мы все знаем,contextПакеты были представлены после Golang 1.17, обычноctxбудет передан в качестве первого параметра функции. Однако экспортируемые функции существующего стабильного API не могут изменить сигнатуру функции и добавить первый входной параметр функции.context.Context, что влияет на все вызывающие функции, особенно в некоторых базах кода низкого уровня, что является очень опасной операцией.

Используйте команду Go新增函数метод решает эту проблему. Возьми каштан,database/sqlэтого пакетаQueryСигнатура метода всегда была:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

когдаcontextКогда пакет был представлен, команда Go добавила такую ​​функцию:

func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)

И изменил только один код:

func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
    return db.QueryContext(context.Background(), query, args...)
}

Таким образом, команда Go может плавно обновитьpackageПри этом на читабельность и совместимость кода это никак не повлияет. Подобный код можно найти повсюду в исходном коде golang.

необязательные аргументы

Если перед реализацией пакета вы решите, что функции может потребоваться добавить параметры для расширения некоторых функций, вы можете заранее использовать необязательные аргументы в сигнатуре функции. Самый простой способ — использовать параметры структуры в сигнатуре функции, ниже приведен исходный код golang.crypto/tls.DialКусок кода:

func Dial(network, addr string, config *Config) (*Conn, error)

Dialреализация функцииTLSОперация рукопожатия требует многих других параметров в этом процессе, а также поддерживает значения по умолчанию. когда даноconfigперечислитьnilКогда используется значение по умолчанию; при передачеConfig structпереопределит значение по умолчанию. Если есть новыйTLSпараметры конфигурации могут быть легкоConfig structЭто делается путем добавления к нему новых полей, что обеспечивает обратную совместимость.

В некоторых случаях добавление функций и использование необязательных параметров можно объединить, превратив структуру необязательных параметров в приемник метода. Например, до Go 1.11,netупаковать вListenСигнатура метода:

func Listen(network, address string) (Listener, error)

Но в Go 1.11 команда Go добавила два новыхfeature :

  1. Параметр контекста передается;
  2. повысилсяcontrol function, позволяя вызывающему абоненту еще не иметь сетевого подключенияbindПри настройке параметров исходного соединения.

Кажется, это значительная корректировка.Если это общий разработчик, будет добавлена ​​не более одной функции, а параметр добавленcontext, control function. Но разработчики команды Go не бездействуют,netАвтор пакета думает, что эту функцию когда-нибудь в будущем настроят, или ей нужно будет больше параметров? Так что я зарезервировал одинListenConfigstruct, реализованная для этого strcutListenметод, так что нет необходимости добавлять еще одну функцию для решения проблемы.

type ListenConfig struct {
    Control func(network, address string, c syscall.RawConn) error
}

func (*ListenConfig) Listen(ctx context.Context, network, address string) (Listener, error)

Существует также шаблон проектирования, называемый необязательным типом, который принимает необязательные функции в качестве параметров функции, и каждая необязательная функция может регулировать свое состояние с помощью параметров. В блоге Роба Пайка (command center.blogspot.com/2014/01/Сатир…) для подробного объяснения этого шаблона. Этот шаблон проектирования широко используется в исходном коде grpc.

option typesс параметром функцииoption structИмеют тот же эффект: они представляют собой расширяемый способ передачи поведения, изменения конфигурации. Решение о том, что выбрать, во многом зависит от конкретного сценария. Взгляните на gRPCDialOptionИспользование типов опционов:

grpc.Dial("some-target",
  grpc.WithAuthority("some-authority"),
  grpc.WithMaxDelay(time.Second),
  grpc.WithBlock())

Конечно, вы также можете сделать это как параметр структуры:

notgrpc.Dial("some-target", &notgrpc.Options{
  Authority: "some-authority",
  MaxDelay:  time.Minute,
  Block:     true,
})

Любой из вышеперечисленных методов является методом, который может поддерживать совместимость модуля Go, и разумная реализация может быть выбрана в соответствии с различными сценариями.

Гарантированная совместимость интерфейсов

Иногда поддержка новых функций требует изменений в открытом (общедоступном) интерфейсе: интерфейс должен быть расширен новыми методами. Нецелесообразно добавлять методы непосредственно в интерфейс, что заставит разработчиков интерфейса изменять код. Итак, как мы можем поддерживать новые методы в открытом интерфейсе?

Совет, данный командой Go, таков: определите новый интерфейс с новыми методами, а затем динамически проверяйте, является ли предоставленный тип старым или новым типом везде, где используется старый интерфейс.

Начнем с исходного кода golang.archive/tarпакет, чтобы объяснить подробно.tar.NewReaderкio.Readerв качестве параметра, но потом команда Go посчитала, что нужно предусмотреть более эффективный способ, то есть при вызовеSeekметод может пропустить заголовок файла. но не напрямуюio.Readerновое вSeekметод, который влияет на все реализации, которыеio.Reader(если вы видели исходный код golang, то знаете, насколько широко используется интерфейс io.Reader).

Другой способ -tar.NeaReaderВходной параметр меняется наio.ReaderSeekerинтерфейс, потому что интерфейс поддерживает обаio.Readerа такжеSeek. Но, как было сказано ранее, изменение сигнатуры функции — не лучший способ.

Поэтому команда Go решила сохранитьtar.NewReaderподпись не изменилась, вReadВведите проверку в методе:

package tar

type Reader struct {
  r io.Reader
}

func NewReader(r io.Reader) *Reader {
  return &Reader{r: r}
}

func (r *Reader) Read(b []byte) (int, error) {
  if rs, ok := r.r.(io.Seeker); ok {
    // Use more efficient rs.Seek.
  }
  // Use less efficient r.r.Read.
}

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

По возможности лучше избегать этой проблемы. Например, при проектировании конструкторов лучше возвращать конкретные типы. В отличие от интерфейсов, использование конкретных типов позволяет вам добавлять новые методы в будущем, не нарушая работу пользователей, и упрощает расширение вашего модуля Go в будущем.

Совет: Если вы используете интерфейс, но не хотите, чтобы пользователи реализовывали его, вы можете добавитьunexportedМетоды.

type TB interface {
    Error(args ...interface{})
    Errorf(format string, args ...interface{})
    // ...

    // A private method to prevent users implementing the
    // interface and so future additions to it will not
    // violate Go 1 compatibility.
// private 避免用户去实现它
    private()
}

Добавить метод конфигурации

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

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

поддерживать совместимость структур

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

Помните, что я сказал вышеnetАвтор пакета добавил его в Go 1.11.ListenConfigструктура? Оказывается, его замысел был правильным. В Go 1.13 появилось новоеKeepAliveПоле, позволяющее отменить или использовать функцию поддержания активности. В предыдущем дизайне добавить это поле было намного проще.

Есть одна деталь в использовании struct, которая может сильно повлиять на пользователей, если вы ее не заметите. Если все поля в структуре эквивалентны (то есть доступны через== or !=для сравнения или может использоваться в качестве ключа карты), то эта структура является эквивалентной. В этом случае, если вы добавите в структуру неравнозначный тип, это приведет к тому, что структура также станет неравнозначной. Если пользователь использует вашу структуру для выполнения эквивалентной операции в коде, он столкнется с ошибкой кода.

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

Conclusion

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

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

                             *官方资讯\*最新技术\*独家解读*