Недавно команда 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
:
- Параметр контекста передается;
- повысился
control function
, позволяя вызывающему абоненту еще не иметь сетевого подключенияbind
При настройке параметров исходного соединения.
Кажется, это значительная корректировка.Если это общий разработчик, будет добавлена не более одной функции, а параметр добавленcontext
, control function
. Но разработчики команды Go не бездействуют,net
Автор пакета думает, что эту функцию когда-нибудь в будущем настроят, или ей нужно будет больше параметров? Так что я зарезервировал одинListenConfig
struct, реализованная для этого 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", ¬grpc.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. Но в большинстве случаев внесение обратно совместимых изменений должно быть вашим первым выбором, чтобы не создавать проблем для пользователей.
*官方资讯\*最新技术\*独家解读*