воскресенье, 29 сентября 2019 г.

Модель памяти Go

Модель памяти Go задает условия, при которых считывания переменной в одной go-процедуре (goroutine) могут гарантированно наблюдать значения, полученные в результате записи в одну и ту же переменную в другой go-процедуре.

Программы, которые изменяют данные, к которым одновременно обращается несколько go-процедур, должны сериализовать такой доступ.

Чтобы сериализовать доступ, защитите данные с помощью операций канала или других примитивов синхронизации, таких как в пакетах sync и sync/atomic.

Происходит до

В рамках одной go-процедуры чтения и записи должны вести себя так, как если бы они выполнялись в порядке, указанном программой. То есть компиляторы и процессоры могут переупорядочивать операции чтения и записи, выполняемые в пределах одной go-процедуры, только когда переупорядочение не изменяет поведение в этой go-процедуре, как определено в спецификации языка. Из-за этого переупорядочения порядок выполнения, наблюдаемый одной go-процедурой, может отличаться от порядка, воспринимаемого другой. Например, если одна go-процедура выполняет a = 1; b = 2; другая может наблюдать обновленное значение b перед обновленным значением a.

Чтобы указать требования для чтения и записи, мы определяем, что происходит до, частичный порядок выполнения операций с памятью в программе Go. Если событие e1 происходит до события e2, то мы говорим, что e2 происходит после e1. Кроме того, если e1 не происходит до e2 и не происходит после e2, то мы говорим, что e1 и e2 происходят конкурентно.

В пределах одной go-процедуры порядок "происходит до" - это порядок, выраженный программой.

Чтение r переменной v может наблюдать запись w в v, если выполняются оба следующих условия:

  • r не происходит до w.
  • Нет другой записи w` в v, которая происходит после w, но до r.

Чтобы гарантировать, что чтение r переменной v наблюдает конкретную запись w в v, убедитесь, что w - единственная запись, которую r позволено наблюдать. То есть r гарантированно наблюдает w, если выполняются оба следующих условия:

  • w происходит до r.
  • Любая другая запись в разделяемую переменную v происходит до w или после r.

Эта пара условий сильнее первой пары; это требует, чтобы не было никаких других записей, происходящих одновременно с w или r.

Внутри одной go-процедуры нет конкурентности, поэтому два определения эквивалентны: чтение r наблюдает значение, записанное самой последней записью w в v. Когда несколько go-процедур обращаются к общей переменной v, они должны использовать события синхронизации, чтобы установить условия "происходит до", обеспечивающие что чтение наблюдает желаемые записи.

Инициализация переменной v с нулевым значением для ее типа ведет себя как запись в модели памяти.

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

Синхронизация


Инициализация

Инициализация программы выполняется в одной go-процедуре, но эта go-процедура может создавать другие go-процедуры, которые выполняются конкурентно.

Если пакет p импортирует пакет q, завершение init функций q происходит до начала любого из p.

Запуск функции main.main происходит после завершения всех init функций.

Создание go-процедуры

Оператор go, который запускает новую go-процедуру, происходит до того, как начинается выполнение go-процедуры.

Например, в этой программе:

var a string

func f() {
 print(a)
}

func hello() {
 a = "hello, world"
 go f()
}

вызов hello напечатает "hello, world" в какой-то момент в будущем (возможно, после возвращения hello).

Разрушение go-процедуры

Не гарантируется, что выход из go-процедуры происходит до какого-либо события в программе. Например, в этой программе:

var a string

func hello() {
 go func() { a = "hello" }()
 print(a)
}

присваивание a не сопровождается никаким событием синхронизации, поэтому не гарантируется, что оно будет наблюдаемо любой другой go-процедурой. На самом деле агрессивный компилятор может удалить весь оператор go.

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

Связь по каналу

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

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

Эта программа:

var c = make(chan int, 10)
var a string

func f() {
 a = "hello, world"
 c <- 0
}

func main() {
 go f()
 <-c
 print(a)
}

гарантированно будет печатать "hello, world". Запись в a происходит до отправки по c, что происходит до завершения соответствующего приема по c, что происходит перед печатью.

Закрытие канала происходит до получения, которое возвращает нулевое значение, потому что канал закрыт.

В предыдущем примере замена c <- 0 на close(c) приводит к программе с таким же гарантированным поведением.

Прием из небуферизованного канала происходит до того, как отправка по этому каналу завершается.

Эта программа (как указано выше, но с помененными местами утверждений отправки и получения и использованием небуферизованного канала):

var c = make(chan int)
var a string

func f() {
 a = "hello, world"
 <-c
}
func main() {
 go f()
 c <- 0
 print(a)
}

также гарантированно будет печатать "hello, world". Запись в a происходит до получения по c, что происходит до завершения соответствующей отправки по c, что происходит перед печатью.

Если бы канал был буферизован (например, c = make(chan int, 1)), то программе не гарантировалась бы печать "hello, world". (Она может напечатать пустую строку, обрушиться, или сделать что-то еще.)

k-й прием по каналу с пропускной способностью C происходит до того, как k+C-ая передача по этому каналу завершается.

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

Эта программа запускает go-процедуру для каждой записи в work списке, но эти go-процедуры координируются использованием limit канала, чтобы гарантировать, что не более трех одновременно выполняют work функции.

var limit = make(chan int, 3)

func main() {
 for _, w := range work {
  go func(w func()) {
   limit <- 1
   w()
   <-limit
  }(w)
 }
 select{}
}

Поскольку обмен данными по нулевым каналам никогда не может быть продолжен, select только с нулевыми каналами (select{}) и без случая по умолчанию блокируется навсегда. Подробней о select здесь и здесь.

Замки (locks)

Пакет sync реализует два типа данных замков, sync.Mutex и sync.RWMutex.

Для любой sync.Mutex или sync.RWMutex переменной l при n < m вызов n из l.Unlock() происходит до возврата вызова m из l.Lock().

Эта программа:

var l sync.Mutex
var a string

func f() {
 a = "hello, world"
 l.Unlock()
}

func main() {
 l.Lock()
 go f()
 l.Lock()
 print(a)
}

гарантированно будет печатать "hello, world". Первый вызов l.Unlock() (в f) происходит до того, как второй вызов l.Lock() (в main) возвращается, что происходит до печати.

Для любого вызова l.RLock sync.RWMutex переменной l существует такое n, что l.RLock происходит (возвращается) после вызова n для l.Unlock, и соотвествующий l.RUnlock происходит до вызова n + 1 для l.Lock.

Once (единожды)

Пакет sync обеспечивает безопасный механизм для инициализации при наличии нескольких go-процедур благодаря использованию типа Once. Несколько потоков могут выполнять once.Do(f) для определенного f, но только один из них будет запускать f(), а остальные вызовы будут блокироваться до тех пор, пока f() не вернется.

Один вызов функции f() из once.Do(f) происходит (возвращается) до того как любой вызов once.Do(f) возвращается.

В этой программе:

var a string
var once sync.Once

func setup() {
 a = "hello, world"
}

func doprint() {
 once.Do(setup)
 print(a)
}

func twoprint() {
 go doprint()
 go doprint()
}

вызов twoprint вызовет setup ровно один раз. Функция setup завершится до любого вызова печати. В результате "hello, world" будет напечатано дважды.

Направильная синхронизация

Обратите внимание, что чтение r может наблюдать значение, записанное записью w, которое происходит одновременно с r. Даже если это происходит, это не означает, что чтения происходящие после r будут наблюдать записи, которые произошли до w.

В этой программе:

var a, b int

func f() {
 a = 1
 b = 2
}

func g() {
 print(b)
 print(a)
}

func main() {
 go f()
 g()
}

может случиться так, что g напечатает 2, а затем 0.

Этот факт лишает законной силы несколько общих идиом.

Двойная проверка блокировки - это попытка избежать накладных расходов на синхронизацию. Например, программа twoprint может быть неправильно написана как:

var a string
var done bool

func setup() {
 a = "hello, world"
 done = true
}

func doprint() {
 if !done {
  once.Do(setup)
 }
 print(a)
}

func twoprint() {
 go doprint()
 go doprint()
}

но нет никакой гарантии, что в doprint наблюдение за записью в done подразумевает наблюдение за записью в a. Эта версия может (неправильно) печатать пустую строку вместо "hello, world".

Другая неправильная идиома занята ожиданием значения, как в:

var a string
var done bool

func setup() {
 a = "hello, world"
 done = true
}

func main() {
 go setup()
 for !done {
 }
 print(a)
}

Как и раньше, нет гарантии, что в main наблюдение за записью в done подразумевает наблюдение за записью в a, поэтому эта программа может также напечатать пустую строку. Хуже того, нет никакой гарантии, что запись в done когда-либо будет выполняться main, поскольку нет никаких событий синхронизации между двумя потоками. Не гарантируется что цикл в main завершится.

На эту тему есть более тонкие варианты, такие как эта программа.

type T struct {
 msg string
}

var g *T

func setup() {
 t := new(T)
 t.msg = "hello, world"
 g = t
}

func main() {
 go setup()
 for g == nil {
 }
 print(g.msg)
}

Даже если main наблюдает за g != nil и выходит из своего цикла, нет гарантии, что он будет наблюдать инициализированное значение для g.msg.

Во всех этих примерах решение одно и то же: используйте явную синхронизацию.


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


суббота, 28 сентября 2019 г.

Go 1.13 заметки о релизе

Последний выпуск Go, версия 1.13, поступает через шесть месяцев после Go 1.12. Большинство его изменений касаются реализации набора инструментов, среды выполнения и библиотек. Как всегда, релиз поддерживает обещание Go 1 о совместимости (Go 1 promise of compatibility). Ожидается, что почти все программы Go будут продолжать компилироваться и запускаться, как и раньше.

Начиная с Go 1.13, команда go по умолчанию загружает и аутентифицирует модули, используя зеркало Go модуля и базу данных контрольной суммы Go, запущенную Google. См. https://proxy.golang.org/privacy для получения информации о конфиденциальности этих служб и документации команды go для получения сведений о конфигурации, в том числе о том, как отключить использование этих серверов или использовать другие. Если вы зависите от непубличных модулей, обратитесь к документации по настройке вашей среды.

Изменения в языке

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

Двоичные целочисленные литералы: префикс 0b или 0B указывает двоичный целочисленный литерал, такой как 0b1011.

Восьмеричные целочисленные литералы: префикс 0o или 0O обозначает восьмеричный целочисленный литерал, такой как 0o660. Существующее восьмеричное обозначение, обозначенное лидирующим 0, за которым следуют восьмеричные цифры, остается в силе.

Шестнадцатеричные литералы с плавающей запятой: теперь можно использовать префикс 0x или 0X для выражения мантиссы числа с плавающей запятой в шестнадцатеричном формате, например 0x1.0p-1021. Шестнадцатеричное число с плавающей точкой всегда должно иметь показатель степени, записанный в виде буквы p или P, за которым следует показатель степени в десятичном виде. Экспонента масштабирует мантиссу на 2 в степени экспоненты.

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

Разделители цифр. Теперь цифры любого числового литерала могут быть разделены (сгруппированы) с помощью символов подчеркивания, таких как 1_000_000, 0b_1010_0110 или 3.1415_9265. Подчеркивание может появляться между любыми двумя цифрами или буквенным префиксом и первой цифрой.

Согласно подписанному предложению о подсчете сдвигов, Go 1.13 снимает ограничение на то, что отсчет сдвигов должен быть без знака. Это изменение устраняет необходимость во многих искусственных преобразованиях uint, введенных исключительно для удовлетворения этого (теперь удаленного) ограничения операторов << и >>.

Эти языковые изменения были реализованы путем изменений в компиляторе и соответствующих внутренних изменений в пакетах библиотеки go/scanner и text/scanner (числовые литералы) и go/types (число сдвигов со знаком).

Если в вашем коде используются модули, а в файлах go.mod указана языковая версия, убедитесь, что для доступа к этим языковым изменениям установлено значение не менее 1.13. Вы можете сделать это, отредактировав файл go.mod напрямую, или запустить go mod edit -go=1.13.

Порты

Go 1.13 - последний выпуск, который будет работать на Native Client (NaCl).

Для GOARCH=wasm, новая переменная среды GOWASM принимает список экспериментальных функций, разделенных запятыми, с помощью которых бинарный файл компилируется.

AIX

AIX на PPC64 (aix/ppc64) теперь поддерживает режимы cgo, внешнее связывание, а также режимы сборки c-archive и pie.

Android

Программы Go теперь совместимы с Android 10.

Darwin

Как было объявлено в примечаниях к выпуску Go 1.12, для Go 1.13 теперь требуется macOS 10.11 El Capitan или более поздняя версия; поддержка предыдущих версий была прекращена.

FreeBSD

Как было объявлено в примечаниях к выпуску Go 1.12, для Go 1.13 теперь требуется FreeBSD 11.2 или более поздняя версия; поддержка предыдущих версий была прекращена. FreeBSD 12.0 или более поздней версии требует ядра с установленным параметром COMPAT_FREEBSD11 (это значение по умолчанию).

Illumos

Go теперь поддерживает Illumos с GOOS=illumos. Тег сборки illumos подразумевает тег сборки solaris.

Windows

Версия Windows, указанная внутренне связанными бинарными файлами Windows, теперь является Windows 7, а не NT 4.0. Это уже была минимально необходимая версия для Go, но она может повлиять на поведение системных вызовов, которые имеют режим обратной совместимости. Теперь они будут вести себя как задокументировано. Внешне связанные бинарные файлы (любая программа, использующая cgo) всегда указывали более новую версию Windows.

Инструменты


Модули

Переменные среды

Переменная среды GO111MODULE по умолчанию продолжает быть auto, но auto настройка теперь активирует режим с учетом модулей (module-aware mode) команды go всякий раз, когда текущий рабочий каталог содержит или находится под каталогом, содержащим файл go.mod - даже если текущий каталог находится в GOPATH/src. Это изменение упрощает миграцию существующего кода в GOPATH/src и текущее обслуживание пакетов с поддержкой модулей наряду с импортерами, не поддерживающими модули.

Новая переменная среды GOPRIVATE указывает пути модулей, которые не являются общедоступными. Он служит значением по умолчанию для низкоуровневых переменных GONOPROXY и GONOSUMDB, которые обеспечивают более точный контроль над тем, какие модули выбираются через прокси и проверяются с использованием базы данных контрольной суммы.

Переменная среды GOPROXY теперь может быть установлена в виде списка прокси-адресов, разделенных запятыми, или специального прямого токена, и его значение по умолчанию теперь https://proxy.golang.org,direct. При разрешении пути пакета к содержащему его модулю команда go будет пытаться выполнить все возможные пути модулей на каждом прокси в списке подряд. Недоступный прокси или код состояния HTTP, отличный от 404 или 410, завершает поиск, не обращаясь к оставшимся прокси.

Новая переменная среды GOSUMDB идентифицирует имя и, необязательно, открытый ключ и URL-адрес сервера базы данных для проверки контрольных сумм модулей, которые еще не перечислены в файле go.sum основного модуля. Если GOSUMDB не включает в себя явный URL-адрес, этот URL-адрес выбирается путем проверки URL-адресов GOPROXY для конечной точки, указывающей поддержку базы данных контрольной суммы, и возвращается к прямому соединению с указанной базой данных, если он не поддерживается каким-либо прокси. Если GOSUMDB отключен, база данных контрольных сумм не используется, и проверяются только существующие контрольные суммы в файле go.sum.

Пользователи, которые не могут получить доступ к базе данных прокси и контрольной суммы по умолчанию (например, из-за конфигурации с межсетевым экраном или изолированной программной средой), могут отключить их использование, установив для GOPROXY значение direct и/или для GOSUMDB значение off. go env -w может использоваться для установки значений по умолчанию для этих переменных независимо от платформы:

go env -w GOPROXY=direct
go env -w GOSUMDB=off

go get

В режиме с поддержкой модулей (module-aware mode) go get с флагом -u теперь обновляет меньший набор модулей, который больше соответствует набору пакетов, обновляемых с помощью go get -u в режиме GOPATH. go get -u продолжает обновлять модули и пакеты, названные в командной строке, но дополнительно обновляет только модули, содержащие пакеты, импортированные именованными пакетами, а не требования к переходным модулям модулей, содержащих названные пакеты.

В частности, обратите внимание, что go get -u (без дополнительных аргументов) теперь обновляет только транзитивный импорт пакета в текущем каталоге. Вместо этого обновите все пакеты, транзитивно импортированные основным модулем (включая тестовые зависимости), используйте команду go get -u all.

В результате указанных выше изменений в go get -u подкоманда go get больше не поддерживает флаг -m, из-за чего go get останавливается перед загрузкой пакетов. Флаг -d остается поддерживаемым и продолжает вызывать остановку go get после загрузки исходного кода, необходимого для построения зависимостей именованных пакетов.

По умолчанию, go get -u в режиме модуля (module mode) обновляет только не тестовые зависимости, как в режиме GOPATH. Теперь он также принимает флаг -t, который (как в режиме GOPATH) заставляет go get включать пакеты, импортированные тестами пакетов, названных в командной строке.

В режиме с поддержкой модулей (module-aware mode) подкоманда go get теперь поддерживает суффикс версии @patch. Суффикс @patch указывает, что названный модуль или модуль, содержащий названный пакет, должен быть обновлен до самого высокого выпуска исправления с теми же основными и вспомогательными версиями, что и версия, найденная в списке сборки.

Если модуль, переданный в качестве аргумента для перехода без суффикса версии, уже требуется в более новой версии, чем последняя выпущенная версия, он останется в более новой версии. Это согласуется с поведением флага -u для зависимостей модуля. Это предотвращает неожиданные снижения версий предварительных версий. Новая версия суффикса @upgrade явно запрашивает такое поведение. @latest явно запрашивает последнюю версию независимо от текущей версии.

Проверка версии

При извлечении модуля из системы управления версиями команда go теперь выполняет дополнительную проверку запрашиваемой строки версии.

Аннотация версии +incompatible обходит требование семантического контроля версий импорта для репозиториев, предшествующих внедрению модулей. Команда go теперь проверяет, что такая версия не включает явный файл go.mod.

Команда go теперь проверяет соответствие между псевдо-версиями и метаданными контроля версий. В частности:

  • Префикс версии должен иметь форму vX.0.0, или производный от тега на предке названной ревизии, или производный от тега, который включает метаданные сборки в самой именованной ревизии.
  • Строка даты должна соответствовать метке времени UTC ревизии.
  • Короткое имя ревизии должно использовать то же количество символов, что и команда go. (Для хэшей SHA-1, используемых git, 12-значный префикс.)

Если директива require в main модуле использует недопустимую псевдо-версию, ее обычно можно исправить, отредактировав версию, просто добавив хеш коммита, и повторно выполнив команду go, например, go list -m all или go mod tidy. Например,

require github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c

можно отредактировать на

require github.com/docker/docker e7b5f7dbe98c

который в настоящее время разрешается к

require github.com/docker/docker v0.7.3-0.20190319215453-e7b5f7dbe98c

Если для одной из транзитивных зависимостей main модуля требуется неверная версия или псевдоверсия, недопустимую версию можно заменить действительной версией с помощью директивы replace в файле go.mod основного модуля. Если замена является хешем коммита, он будет преобразован в соответствующую псевдо-версию, как указано выше. Например,

replace github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c => github.com/docker/docker e7b5f7dbe98c

в настоящее время разрешается к

replace github.com/docker/docker v1.14.0-0.20190319215453-e7b5f7dbe98c => github.com/docker/docker v0.7.3-0.20190319215453-e7b5f7dbe98c

Команда go

Команда go env теперь принимает флаг -w для установки значения по умолчанию для каждого пользователя переменной среды, распознаваемой командой go, и соответствующий флаг -u для сброса ранее установленного значения по умолчанию. Значения по умолчанию, установленные с помощью go env -w, хранятся в файле go/env в os.UserConfigDir().

Команда go version теперь принимает аргументы именования исполняемых файлов и каталогов. При вызове исполняемого файла go version печатает версию Go, использованную для сборки исполняемого файла. Если используется флаг -m, go version печатает информацию о версии встроенного модуля исполняемого файла, если она доступна. При вызове в каталоге go version печатает информацию об исполняемых файлах, содержащихся в каталоге и его подкаталогах.

Новый флаг go build -trimpath удаляет все пути файловой системы из скомпилированного исполняемого файла, чтобы улучшить воспроизводимость сборки.

Если флаг -o, переданный в go build, ссылается на существующий каталог, go build теперь будет записывать исполняемые файлы в этом каталоге для main пакетов, соответствующих его аргументам пакета.

Флаг go build -tags теперь принимает разделенный запятыми список тегов сборки, что позволяет использовать несколько тегов в GOFLAGS. Разделенная пробелами форма устарела, но все еще распознается и будет сохранена.

go generate now устанавливает тег create build, чтобы в файлах можно было искать директивы, но игнорировать их во время сборки.

Как было объявлено в примечаниях к релизу Go 1.12, пакеты только для бинарного кода больше не поддерживаются. Сборка бинарного пакета (помеченного комментарием //go:binary-only-package) теперь приводит к ошибке.

Цепочка инструментов компилятора (Compiler toolchain)

Компилятор имеет новую реализацию анализа побега (escape analysis), которая является более точной. Для большей части кода Go должно быть улучшение (другими словами, больше переменных и выражений Go, размещаемых в стеке, а не в куче). Однако эта повышенная точность также может привести к поломке недействительного кода, который работал раньше (например, код, который нарушает правила безопасности unsafe.Pointer). Если вы заметили какие-либо регрессии, которые кажутся связанными, старый проход анализа выхода можно снова включить с помощью команды go build -gcflags=all=-newescape=false. Возможность использовать старый escape-анализ будет удалена в следующем выпуске.

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

Ассемблер

Ассемблер теперь поддерживает многие атомарные инструкции, представленные в ARM v8.1.

gofmt

gofmt (и с этим go fmt) теперь канонизирует числовые буквенные префиксы и экспоненты, чтобы использовать строчные буквы, но оставляет шестнадцатеричные цифры в покое. Это улучшает читаемость при использовании нового восьмеричного префикса (0O становится 0o), и перезапись применяется последовательно. Теперь gofmt также удаляет ненужные начальные нули из десятичного целого мнимого литерала. (Для обратной совместимости целочисленный мнимый литерал, начинающийся с 0, считается десятичным, а не восьмеричным числом. Удаление лишних начальных нулей позволяет избежать путаницы.) Например, 0B1010, 0XabcDEF, 0O660, 1.2E3 и 01i становятся 0b1010, 0xabcDEF , 0o660, 1.2e3 и 1i после применения gofmt.

godoc и go doc

Веб-сервер godoc больше не входит в основной бинарный дистрибутив. Чтобы запустить веб-сервер godoc локально, сначала установите его вручную:

go get golang.org/x/tools/cmd/godoc
godoc

Команда go doc теперь всегда включает в свой пакет предложение package, за исключением команд. Это заменяет предыдущее поведение, в котором использовалась эвристика, в результате чего при определенных условиях предложение пакета было опущено.

Среда выполнения (runtime)

Сообщения о panic вне диапазона (out of range) теперь включают индекс, который вышел за пределы, и длину (или емкость) среза. Например, s[3] на срезе длиной 1 будет паниковать с "runtime error: index out of range [3] with length 1".

Этот релиз улучшает производительность большинства применений defer на 30%.

Среда выполнения теперь более агрессивна при возврате памяти операционной системе, чтобы сделать ее доступной для приложений-арендаторов. Ранее среда выполнения могла сохранять память в течение пяти или более минут после скачка размера кучи. Теперь он начнет возвращать его сразу после сокращения кучи. Однако во многих ОС, включая Linux, сама ОС лениво восстанавливает память, поэтому процесс RSS не будет уменьшаться до тех пор, пока система не окажется под давлением памяти.

Основная библиотека (Core library)


TLS 1.3

Как было объявлено в Go 1.12, Go 1.13 по умолчанию включает поддержку TLS 1.3 в пакете crypto/tls. Его можно отключить, добавив значение tls13=0 в переменную среды GODEBUG. Отказ от участия будет удален в Go 1.14.

crypto/ed25519

Новый пакет crypto/ed25519 реализует схему подписи Ed25519. Эта функциональность была ранее предоставлена ​​пакетом golang.org/x/crypto/ed25519, который становится оболочкой для crypto/ed25519 при использовании с Go 1.13+.

Оборачивание ошибок

В версии 1.13 содержится поддержка оборачивания ошибок.

Ошибка e может обернуть другую ошибку w, предоставив метод Unwrap, который возвращает w. И e, и w доступны программам, что позволяет e предоставлять дополнительный контекст для w или переинтерпретировать его, в то же время позволяя программам принимать решения на основе w.

Для поддержки переноса в fmt.Errorf теперь есть глагол %w для создания упакованных ошибок, а три новые функции в пакете errors (errors.Unwrap, errors.Is и errors.As) упрощают развертывание и проверку упакованных ошибок.

Незначительные изменения в библиотеке

Как всегда, в библиотеку внесены различные незначительные изменения и обновления, сделанные с учетом требований совместимости Go 1.

bytes

Новая функция ToValidUTF8 возвращает копию заданного байтового среза, причем каждый прогон недопустимых байтовых последовательностей UTF-8 заменяется заданным срезом.

context

Форматирование контекстов, возвращаемых WithValue, больше не зависит от fmt и не будет преобразовываться в строку таким же образом. Код, который зависит от точной предыдущей классификации, может быть затронут.

crypto/tls

Поддержка SSL версии 3.0 (SSLv3) устарела и будет удалена в Go 1.14. Обратите внимание, что SSLv3 - это криптографически взломанный протокол, предшествующий TLS.

SSLv3 всегда был отключен по умолчанию, за исключением Go 1.12, когда он был ошибочно включен по умолчанию на стороне сервера. Теперь он снова отключен по умолчанию. (SSLv3 никогда не поддерживался на стороне клиента.)

Сертификаты Ed25519 теперь поддерживаются в версиях TLS 1.2 и 1.3.

crypto/x509

Ключи Ed25519 теперь поддерживаются в сертификатах и ​​запросах сертификатов в соответствии с RFC 8410, а также функциями ParsePKCS8PrivateKey, MarshalPKCS8PrivateKey и ParsePKIXPublicKey.

Пути для поиска системных root теперь включают /etc/ssl/cert.pem для поддержки расположения по умолчанию в Alpine Linux 3.7+.

database/sql

Новый тип NullTime представляет time. Time может быть нулевым.

Новый тип NullInt32 представляет int32, который может быть нулевым.

debug/dwarf

Метод Data.Type больше не паникует, если он встречает неизвестный тег DWARF на графе типов. Вместо этого он представляет этот компонент типа с объектом UnsupportedType.

errors

Новая функция As находит первую ошибку в цепочке заданной ошибки (последовательность обернутых ошибок), которая соответствует типу заданной цели, и, если это так, устанавливает для цели значение этой ошибки.

Новая функция Is сообщает, соответствует ли данное значение ошибки ошибке в другой цепочке ошибок.

Новая функция Unwrap возвращает результат вызова Unwrap для данной ошибки, если она существует.

fmt

Печатные глаголы %x и %X теперь форматируют числа с плавающей точкой и комплексные числа в шестнадцатеричном формате, соответственно в нижнем и верхнем регистре.

Новый печатный глагол %O форматирует целые числа в базе 8, испуская префикс 0o.

Сканер теперь принимает шестнадцатеричные значения с плавающей точкой, подчеркивания с разделением цифр и префиксы 0b и 0o.

Функция Errorf имеет новый глагол, %w, операнд которого должен быть ошибкой. Ошибка, возвращаемая из Errorf, будет иметь метод Unwrap, который возвращает операнд %w.

go/scanner

Сканер был обновлен для распознавания новых числовых литералов Go, в частности бинарных литералов с префиксом 0b/0B, восьмеричных литералов с префиксом 0o/0O и чисел с плавающей точкой с шестнадцатеричной мантиссой. Мнимый суффикс i теперь может использоваться с любым числовым литералом, а подчеркивания могут использоваться в качестве разделителей цифр для группировки.

go/types

Проверка типов была обновлена ​​в соответствии с новыми правилами для целочисленных сдвигов.

html/template

При использовании тега <script> с атрибутом "module" в качестве атрибута type код теперь будет интерпретироваться как скрипт модуля JavaScript.

log

Новая функция Writer возвращает назначение вывода для стандартного logger.

math/big

Новый метод Rat.SetUint64 устанавливает для Rat значение uint64.

Для Float.Parse, если base равно 0, подчеркивания могут использоваться между цифрами для удобства чтения.

Для Int.SetString, если base равен 0, для удобства чтения между цифрами могут использоваться подчеркивания между цифрами.

Rat.SetString теперь принимает недесятичные представления с плавающей точкой.

math/bits

Время выполнения Add, Sub, Mul, RotateLeft и ReverseBytes теперь гарантированно не зависит от входных данных.

net

В системах Unix, где use-vc установлен в resolv.conf, для разрешения DNS используется TCP.

Новое поле ListenConfig.KeepAlive указывает период поддержания активности для сетевых подключений, принятых слушателем. Если это поле равно 0 (по умолчанию), TCP keep-alives будет включен. Чтобы отключить их, установите для него отрицательное значение.

Обратите внимание, что ошибка, возвращаемая из операций ввода-вывода в соединении, которое было закрыто тайм-аутом проверки активности, будет иметь метод Timeout, который при вызове возвращает true. Это может затруднить распознавание ошибки поддержания активности из-за пропущенного срока, установленного методом SetDeadline и аналогичными методами. Код, который использует крайние сроки и проверяет их с помощью метода Timeout или os.IsTimeout, может захотеть отключить keep-alives или использовать error.Is (syscall.ETIMEDOUT) (в системах Unix), который вернет true для тайм-аута keep-alive и false для срока ожидания.

net/http

Новые поля Transport.WriteBufferSize и Transport.ReadBufferSize позволяют указывать размеры буферов записи и чтения для Transport. Если любое из полей равно нулю, используется размер по умолчанию 4 KB.

Новое поле Transport.ForceAttemptHTTP2 определяет, включен ли HTTP/2, если указан ненулевой набор Dial, DialTLS или DialContext функция или TLSClientConfig.

Transport.MaxConnsPerHost теперь корректно работает с HTTP/2.

ResponseWriter для TimeoutHandler теперь реализует интерфейсы Pusher и Flusher.

StatusCode 103 "Early Hints" был добавлен.

Transport теперь использует реализацию io.ReaderFrom для Request.Body, если она доступна, для оптимизации написания тела.

При обнаружении неподдерживаемых кодировок передачи http.Server теперь возвращает статус "501 Unimplemented", как того требует спецификация HTTP RFC 7230, раздел 3.3.1.

Для Server новые поля BaseContext и ConnContext позволяют более точно контролировать значения контекста, предоставляемые запросам и соединениям.

http.DetectContentType теперь корректно обнаруживает подписи RAR, а также может теперь обнаруживать подписи RAR v5.

Новый метод Clone для Header возвращает копию получателя.

Была добавлена ​​новая функция NewRequestWithContext, которая принимает Context, который управляет всем временем жизни созданного исходящего Request, подходящий для использования с Client.Do и Transport.RoundTrip.

Transport больше не регистрирует ошибки, когда серверы корректно закрывают незанятые соединения, используя ответ "408 Request Timeout".

os

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

Если File открывается с использованием флага O_APPEND, его метод WriteAt всегда будет возвращать ошибку.

os/exec

В Windows среда для Cmd всегда наследует значение %SYSTEMROOT% родительского процесса, если только поле Cmd.Env не содержит для него явного значения.

reflect

Новый метод Value.IsZero сообщает, является ли значение нулевым значением для его типа.

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

runtime

Трейсбеки, runtime.Caller и runtime.Callers теперь ссылаются на функцию, которая инициализирует глобальные переменные PKG как PKG.init вместо PKG.init.ializers.

strconv

Для strconv.ParseFloat, strconv.ParseInt и strconv.ParseUint, если base равно 0, подчеркивания могут использоваться между цифрами для удобства чтения.

strings

Новая функция ToValidUTF8 возвращает копию заданной строки с каждым запуском недопустимых последовательностей байтов UTF-8, замененных данной строкой.

sync

Быстрые пути Mutex.Lock, Mutex.Unlock, RWMutex.Lock, RWMutex.RUnlock и Once.Do теперь встроены в своих вызывающих. Для неосторожных случаев на amd64 эти изменения делают Once.Do в два раза быстрее, а методы Mutex/RWMutex - на 10% быстрее.

Большой Pool больше не увеличивает время паузы в остановке мира (stop-the-world).

Pool больше не нужно полностью заполнять после каждого сбора мусора. Теперь он сохраняет некоторые объекты в GC, в отличие от освобождения всех объектов, что снижает скачки нагрузки для активных пользователей Pool.

syscall

Использование _getdirentries64 было удалено из сборок Darwin, чтобы позволить загружать бинарные файлы Go в MacOS App Store.

Новые поля ProcessAttributes и ThreadAttributes в SysProcAttr были введены для Windows, предоставляя настройки безопасности при создании новых процессов.

EINVAL больше не возвращается в нулевом режиме Chmod в Windows.

syscall/js

TypedArrayOf был заменен CopyBytesToGo и CopyBytesToJS для копирования байтов между байтовым срезом и Uint8Array.

testing

При выполнении тестов B.N больше не округляется.

Новый метод B.ReportMetric позволяет пользователям сообщать о пользовательских метриках и переопределять встроенные метрики.

Флаги тестирования теперь регистрируются в новой функции Init, которая вызывается сгенерированной main функцией для теста. В результате флаги тестирования теперь регистрируются только при запуске тестового бинарного файла, а пакеты, которые вызывают flag.Parse во время инициализации пакета, могут привести к сбою тестов.

text/scanner

Сканер был обновлен для распознавания новых числовых литералов числа Go, в частности бинарных литералов с префиксом 0b/0B, восьмеричных литералов с префиксом 0o/0O и чисел с плавающей точкой с шестнадцатеричной мантиссой. Кроме того, новый режим AllowDigitSeparators позволяет числовым литералам содержать подчеркивания в качестве разделителей цифр (по умолчанию отключено для обратной совместимости).

text/template

Новая функция slice возвращает результат нарезки первого аргумента по следующим аргументам.

time

День года теперь поддерживается Format и Parse.

Новые методы Duration, микросекунды и миллисекунды, возвращают длительность в виде целого числа их соответственно названных единиц измерения.

unicode

Пакет unicode и связанная с ним поддержка по всей системе были обновлены с Unicode 10.0 до Unicode 11.0, что добавляет 684 новых символа, включая семь новых скриптов и 66 новых эмодзи.


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


пятница, 27 сентября 2019 г.

Дженерики в Golang

Эта статья о том, что будет означать добавление дженериков в Go, и почему это следует сделать. Также будет затронута тема возможного дизайна для добавления дженериков в Go.

Go был выпущен 10 ноября 2009 года. Менее чем через 24 часа появился первый комментарий о дженериках. (В этом комментарии также упоминаются исключения, которые были добавлены к языку, в форме panic и recover в начале 2010 года.)

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

Почему дженерики?

Но что значит добавить дженерики, и зачем это нужно?

Перефразируя Jazayeri: Универсальное программирование позволяет представлять функции и структуры данных в универсальной форме с факторизацией типов.

Что это означает?

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

Допустим, это срез int.

func ReverseInts(s []int) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

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

Теперь возьмем срез строк.

func ReverseStrings(s []string) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

Если вы сравните ReverseInts и ReverseStrings, вы увидите, что эти две функции абсолютно одинаковы, за исключением типа параметра.

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

Большинство других языков позволяют вам писать такие функции.

В динамически типизированном языке, таком как Python или JavaScript, вы можете просто написать функцию, не утруждая себя указанием типа элемента. Это не работает в Go, потому что Go имеет статическую типизацию и требует, чтобы вы записали точный тип среза и тип элементов среза.

Большинство других статически типизированных языков, таких как C++ или Java или Rust или Swift, поддерживают обобщенные типы (дженерики) для решения именно такого рода проблем.

Обобщенное программирование в Go сегодня

Так как же люди пишут такой код на Go?

В Go вы можете написать одну функцию, которая работает для разных типов срезов, используя тип интерфейса и определяя метод для типов срезов, которые вы хотите передать. Именно так работает функция sort.Sort стандартной библиотеки.

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

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

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

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

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

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

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

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

Что дженерики могут принести в Go

Первая и самая важная вещь, которая ожидается от дженериков в Go, - это возможность писать такие функции, как Reverse, не заботясь о типе элемента среза. Мы хотим выделить этот тип элемента. Затем мы можем написать функцию один раз, написать тесты один раз, поместить их в пакет и вызывать их, когда захотим.

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

На данный момент следует сказать, что "дженерики" могут означать много разных вещей. В частности, они означают шаблоны, встречающиеся в языке C++, которые поддерживают немного больше, чем то, что здесь написано.

Здесь подробно рассмотрен Reverse, но есть много других функций, которые можно написать в общем виде, такие как:

  • Найти самый маленький/самый большой элемент в срезе
  • Найти среднее/стандартное отклонение среза
  • Вычислить объединение/пересечение карт
  • Найти кратчайший путь в графе узлов/ребер
  • Применить функцию преобразования к срезу/карте, возвращая новый срез/карту

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

Есть также примеры, которые характерны для Go с его сильной поддержкой конкурентности.

  • Читать из канала с тайм-аутом
  • Объединить два канала в один канал
  • Вызывать список функций параллельно, возвращая часть результатов
  • Вызывать список функций, используя Context, вернуть результат первой функции для завершения, отменить и очистить лишние go-процедуры

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

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

Также, как упомянуто выше, это не просто функции. Это также структуры данных.

Go имеет две универсальные структуры данных общего назначения, встроенные в язык: срезы (slice) и карты (map). Срезы и карты могут содержать значения любого типа данных, при этом статический тип проверяет сохраненные и извлеченные значения. Значения хранятся как сами по себе, а не как типы интерфейсов. То есть, когда есть []int, срез хранит целые числа напрямую, а не целые числа, преобразованные в тип интерфейса.

Срезы и карты являются наиболее полезными общими структурами данных, но они не единственные. Вот еще несколько примеров.

  • Наборы (Sets)
  • Самобалансирующиеся деревья, с эффективной вставкой и обходом в отсортированном порядке
  • Мультикарты с несколькими экземплярами ключа
  • Конкурентные хэш-карты, поддерживающие конкурентные вставки и поиск без единой блокировки

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

Также должна быть возможность использовать алгоритмы, подобные упомянутым ранее, и применять их к общим структурам данных.

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

Вот что Go может извлечь из дженериков. Дженерики могут дать мощные строительные блоки, которые позволят легче делиться кодом и создавать программы.

Преимущества и затраты

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

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

Вот некоторые рекомендации, которым будет необходимо следовать при реализации дженериков.

* Минимизировать новые концепции

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

* Сложность ложится на автора общего кода, а не на пользователя

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

* Писатель и пользователь могут работать независимо друг от друга

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

* Короткое время сборки, быстрое время выполнения

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

* Сохраняйте ясность и простоту Go

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

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

Эскизный дизайн дженериков в Go

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

В Gophercon 2019 Robert Griesemer и Ian Lance Taylor опубликовали черновик проекта для добавления дженериков в Go. Здесь будут рассмотрены некоторые основные моменты.

Вот общая функция Reverse в этом дизайне.

func Reverse (type Element) (s []Element) {
    first := 0
    last := len(s) - 1
    for first < last {
        s[first], s[last] = s[last], s[first]
        first++
        last--
    }
}

Вы заметите, что тело функции точно такое же. Только подпись изменилась.

Тип элемента среза был исключен. Теперь он называется Element и стал тем, что называется параметром типа. Вместо того, чтобы быть частью типа параметра slice, теперь это отдельный дополнительный параметр типа.

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

func ReverseAndPrint(s []int) {
    Reverse(int)(s)
    fmt.Println(s)
}

Этот (int) видим после Reverse в данном примере.

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

Вызов универсальной функции выглядит как вызов любой другой функции.

func ReverseAndPrint(s []int) {
    Reverse(s)
    fmt.Println(s)
}

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

Контракты

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

Функция Reverse может работать со срезами любого типа. Единственное, что она делает со значениями типа Element, это присваивание, которое работает с любым типом в Go. Для такого рода универсальной функции, которая является очень распространенным случаем, не нужно говорить ничего особенного о параметре типа.

Рассмотрим другую функцию.

func IndexByte (type T Sequence) (s T, b byte) int {
    for i := 0; i < len(s); i++ {
        if s[i] == b {
            return i
        }
    }
    return -1
}

В настоящее время и пакет bytes, и пакет strings в стандартной библиотеке имеют функцию IndexByte. Эта функция возвращает индекс b в последовательности s, где s является либо string, либо []byte. Можно было бы использовать эту единую универсальную функцию для замены двух функций в пакетах bytes и strings. На практике можно не беспокоиться об этом, но это полезный простой пример.

Здесь нужно знать, что параметр типа T действует как string или []byte. Мы можем вызвать len, и мы можем индексировать его, и мы можем сравнить результат операции index с байтовым значением.

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

Вот как контракт Sequence определяется для этого примера.

contract Sequence(T) {
    T string, []byte
}

Это довольно просто, поскольку это простой пример: параметр типа T может быть либо string, либо []byte. Здесь контракт может быть новым ключевым словом или специальным идентификатором, распознаваемым в области действия пакета.

Контракты позволяют указать базовый тип параметра типа и/или перечислить методы параметра типа. Они также позволяют описать отношения между параметрами разных типов.

Контракты с методами

Вот еще один простой пример функции, которая использует метод String для возврата []string строкового представления всех элементов в s.

func ToStrings (type E Stringer) (s []E) []string {
    r := make([]string, len(s))
    for i, v := range s {
        r[i] = v.String()
    }
    return r
}

Это довольно просто: пройтись по срезу, вызвать метод String для каждого элемента и вернуть срез из полученных строк.

Эта функция требует, чтобы тип элемента реализовал метод String. Контракт Stringer гарантирует это.

contract Stringer(T) {
    T String() string
}

Контракт просто говорит, что T должен реализовать метод String.

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

Контракты с несколькими типами

Вот пример контракта с несколькими параметрами типа.

type Graph (type Node, Edge G) struct { ... }

contract G(Node, Edge) {
    Node Edges() []Edge
    Edge Nodes() (from Node, to Node)
}

func New (type Node, Edge G) (nodes []Node) *Graph(Node, Edge) {
    ...
}

func (g *Graph(Node, Edge)) ShortestPath(from, to Node) []Edge {
    ...
}

Здесь описывается граф, построенный из узлов и ребер. Не требуется конкретной структуры данных для графа. Вместо этого мы говорим, что тип Node должен иметь метод Edges, который возвращает список ребер, которые соединяются с Node. И тип Edge должен иметь метод Nodes, который возвращает два Node, которые соединяет Edge.

Реализация здесь пропущена, но она показывает сигнатуру функции New, которая возвращает Graph, и сигнатуру метода ShortestPath в Graph.

Важным выводом здесь является то, что контракт - это не просто один тип. Он может описывать отношения между двумя или более типами.

Упорядоченные (Ordered) типы

Одна удивительно распространенная жалоба на Go состоит в том, что у него нет функции Min. Или функции Max. Функции Min нет потому, что полезная функция Min должна работать для любого упорядоченного типа, что означает, что она должна быть универсальной.

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

func Min (type T Ordered) (a, b T) T {
    if a < b {
        return a
    }
    return b
}

Ordered контракт говорит, что тип T должен быть упорядоченным типом, что означает, что он поддерживает операторы типа меньше, больше, и так далее.

contract Ordered(T) {
    T int, int8, int16, int32, int64,
        uint, uint8, uint16, uint32, uint64, uintptr,
        float32, float64,
        string
}

Ordered контракт - это просто список всех упорядоченных типов, определенных языком. Этот контракт принимает любой из перечисленных типов или любой именованный тип, базовый тип которого является одним из этих типов. В основном, любой тип, который вы можете использовать с оператором <.

Оказывается, гораздо проще просто перечислить типы, которые поддерживают оператор <, чем изобрести новую нотацию, которая работает для всех операторов. Ведь в Go только встроенные типы поддерживают операторы.

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

На практике этот контракт, вероятно, попадет в стандартную библиотеку. И поэтому действительно функция Min (которая, вероятно, также будет где-то в стандартной библиотеке) будет выглядеть следующим образом. Здесь идет просто ссылка на Ordered контракт, определенный в пакете contracts.

func Min (type T contracts.Ordered) (a, b T) T {
    if a < b {
        return a
    }
    return b
}

Общие структуры данных

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

type Tree (type E) struct {
    root    *node(E)
    compare func(E, E) int
}

type node (type E) struct {
    val         E
    left, right *node(E)
}

Вот как можно создать новое бинарное дерево. Функция сравнения передается функции New.

func New (type E) (cmp func(E, E) int) *Tree(E) {
    return &Tree(E){compare: cmp}
}

Неэкспортированный метод возвращает указатель либо на слот, содержащий v, либо на место в дереве, куда он должен идти.

func (t *Tree(E)) find(v E) **node(E) {
    pn := &t.root
    for *pn != nil {
        switch cmp := t.compare(v, (*pn).val); {
        case cmp < 0:
            pn = &(*pn).left
        case cmp > 0:
            pn = &(*pn).right
        default:
            return pn
        }
    }
    return pn
}

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

Вот код для проверки, содержит ли дерево значение.

func (t *Tree(E)) Contains(v E) bool {
    return *t.find(e) != nil
}

Вот код для вставки нового значения.

func (t *Tree(E)) Insert(v E) bool {
    pn := t.find(v)
    if *pn != nil {
        return false
    }
    *pn = &node(E){val: v}
    return true
}

Обратите внимание на аргумент типа E для типа node. Вот как выглядит общая структура данных. Как вы можете видеть, это похоже на написание обычного кода Go, за исключением того, что некоторые аргументы типа разбросаны здесь и там.

Использовать дерево довольно просто.

var intTree = tree.New(func(a, b int) int { return a - b })

func InsertAndCheck(v int) {
    intTree.Insert(v)
    if !intTree.Contains(v) {
        log.Fatalf("%d not found after insertion", v)
    }
}

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


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


пятница, 20 сентября 2019 г.

Строка, байт, руна, символ в Golang

В этом посте обсуждаются строки в Go. Поначалу строки могут показаться слишком простой темой для поста в блоге, но для их правильного использования требуется понимание не только того, как они работают, но и разницы между байтом (byte), символом и руной (rune), разницы между Unicode и UTF-8, разницы между строкой и строковым литералом и другие различия.

Что такое строка?

Начнем с некоторых основ.

В Go строка в действительности является срезом (slice) байтов, доступным только для чтения.

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

Вот строковый литерал (подробнее о них ниже), который использует нотацию \xNN для определения строковой константы, содержащей некоторые специфические байтовые значения. (Конечно, байты варьируются в шестнадцатеричном представлении от 00 до FF включительно.)

const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"

Печать строк

Поскольку некоторые байты в нашем примере строки не являются допустимыми ASCII, даже не допустимыми UTF-8, прямая печать строки приведет к ужасному выводу. Простая печать

fmt.Println(sample)

выводит этот беспорядок (точный вид зависит от окружения запуска):

�� = � ⌘

Чтобы выяснить, что на самом деле содержит эта строка, нам нужно разобрать ее на части и изучить кусочки. Есть несколько способов сделать это. Наиболее очевидным является цикл по его содержимому и индивидуальное извлечение байтов, как в этом цикле for:

for i := 0; i < len(sample); i++ {
    fmt.Printf("%x ", sample[i])
}

Как и предполагалось, индексирование строки обращается к отдельным байтам, а не к символам. Мы вернемся к этой теме подробно ниже. А пока давайте придерживаться только байтов. Это вывод из байтового цикла:

bd b2 3d bc 20 e2 8c 98

Обратите внимание, как отдельные байты соответствуют шестнадцатеричным экранировкам, которые определили строку.

Более короткий способ создания презентабельного вывода для грязной строки - использовать %x (шестнадцатеричный) формат в fmt.Printf. Он просто выводит последовательные байты строки в виде шестнадцатеричных цифр, по две на байт.

fmt.Printf("%x\n", sample)

Сравните его вывод с вышеупомянутым:

bdb23dbc20e28c98

Хорошим трюком является использование "пробел" флага в этом формате, помещая пробел между % и x. Сравните используемый здесь метод форматирования строки с приведенным выше,

fmt.Printf("% x\n", sample)

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

bd b2 3d bc 20 e2 8c 98

Есть еще кое-что. Форма %q (в кавычках) будет экранировать любые непечатаемые последовательности байтов в строке, поэтому вывод будет однозначным.

fmt.Printf("%q\n", sample)

Этот метод удобен, когда большая часть строки понятна как текст, но есть особенности, которые нужно искоренить; он производит:

"\xbd\xb2=\xbc ⌘"

Если мы взглянем пристально на этот вывод, то увидим, что в шуме погребен один знак равенства ASCII вместе с обычным пробелом, и в конце появляется известный шведский символ "Достопримечательность" ("Place of Interest" symbol). Этот символ имеет значение Unicode U+2318, закодированное как UTF-8 байтами после пробела (шестнадцатеричное значение 20): e2 8c 98.

Если мы незнакомы или смущены странными значениями в строке, мы можем использовать "плюс" флаг к %q. Этот флаг заставляет выходные данные экранировать не только непечатаемые последовательности, но также и любые байты, отличные от ASCII, при интерпретации UTF-8. В результате он предоставляет Unicode значения правильно отформатированного UTF-8, который представляет не-ASCII данные в строке:

fmt.Printf("%+q\n", sample)

В этом формате Unicode значение для шведского символа отображается как \u экранированными:

"\xbd\xb2=\xbc \u2318"

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

Вот полный набор параметров печати, которые мы перечислили, представленные как полная программа:

package main

import "fmt"

func main() {
    const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"

    fmt.Println("Println:")
    fmt.Println(sample)

    fmt.Println("Byte loop:")
    for i := 0; i < len(sample); i++ {
        fmt.Printf("%x ", sample[i])
    }
    fmt.Printf("\n")

    fmt.Println("Printf with %x:")
    fmt.Printf("%x\n", sample)

    fmt.Println("Printf with % x:")
    fmt.Printf("% x\n", sample)

    fmt.Println("Printf with %q:")
    fmt.Printf("%q\n", sample)

    fmt.Println("Printf with %+q:")
    fmt.Printf("%+q\n", sample)
}

Выполнить в песочнице

UTF-8 и строковые литералы

Как мы видели, индексирование строки дает ее байты, а не символы: строка - это просто набор байтов. Это означает, что когда мы храним символьное значение в строке, мы храним его представление в байтах-за-раз. Давайте посмотрим на более контролируемый пример, чтобы увидеть, как это происходит.

Вот простая программа, которая печатает строковую константу с одним символом тремя различными способами, один раз как обычную строку, один раз как только-ASCII строку в кавычках и один раз как отдельные байты в шестнадцатеричном формате. Чтобы избежать путаницы, мы создаем "сырую строку" (raw string), заключенную в обратные кавычки, чтобы она могла содержать только буквальный текст. (Обычные строки, заключенные в двойные кавычки, могут содержать escape-последовательности, как мы показали выше.)

func main() {
    const placeOfInterest = `⌘`

    fmt.Printf("plain string: ")
    fmt.Printf("%s", placeOfInterest)
    fmt.Printf("\n")

    fmt.Printf("quoted string: ")
    fmt.Printf("%+q", placeOfInterest)
    fmt.Printf("\n")

    fmt.Printf("hex bytes: ")
    for i := 0; i < len(placeOfInterest); i++ {
        fmt.Printf("%x ", placeOfInterest[i])
    }
    fmt.Printf("\n")
}

Запустить в песочнице

Вывод:

plain string: ⌘
quoted string: "\u2318"
hex bytes: e2 8c 98

что напоминает нам, что значение Unicode символа U+2318, символ "Достопримечательность" ⌘, представлено байтами e2 8c 98, и что эти байты являются кодировкой UTF-8 шестнадцатеричного значения 2318.

Это может быть очевидным или нет, в зависимости от вашего знакомства с UTF-8, но стоит потратить некоторое время на объяснение того, как было создано представление строки в UTF-8. Простой факт: оно было создано, когда исходный код был написан.

Исходный код на Go определен как текст UTF-8; никакое другое представление не допускается. Это означает, что когда в исходном коде мы пишем текст

`⌘`

текстовый редактор, использованный для создания программы, помещает кодировку UTF-8 символа ⌘ в исходный текст. Когда мы распечатываем шестнадцатеричные байты, мы просто выгружаем данные, которые редактор поместил в файл.

Короче говоря, исходный код Go - это UTF-8, поэтому исходный код строкового литерала - это текст UTF-8. Если этот строковый литерал не содержит escape-последовательностей, чего не может необработанная строка, созданная строка будет содержать именно исходный текст между кавычками. Таким образом, по определению и по конструкции необработанная строка всегда будет содержать действительное представление UTF-8 своего содержимого. Точно так же, если только он не содержит выводов, нарушающих UTF-8, как в предыдущем разделе, обычный строковый литерал также всегда будет содержать действительный UTF-8.

Некоторые люди думают, что строки Go - это всегда UTF-8, но это не так: только строковые литералы являются UTF-8. Как мы показали в предыдущем разделе, строковые значения могут содержать произвольные байты; как мы показали в этом, строковые литералы всегда содержат текст UTF-8, если у них нет экранирования на уровне байтов.

Подводя итог, можно сказать, что строки могут содержать произвольные байты, но при построении из строковых литералов эти байты (почти всегда) являются UTF-8.

Кодовые точки, символы и руны

До сих пор мы были очень осторожны в том, как мы используем слова "байт" и "символ". Это отчасти потому, что строки содержат байты, а отчасти потому, что понятие "символ" немного сложно определить. Стандарт Unicode использует термин "кодовая точка" ("code point") для обозначения элемента, представленного одним значением. Кодовая точка U+2318 с шестнадцатеричным значением 2318 представляет символ ⌘.

Чтобы выбрать более прозаичный пример, кодовая точка Unicode U+0061 - это строчная латинская буква 'A': a.

Но как насчет строчной буквы с акцентом 'A', à? Это символ, и это также кодовая точка (U+00E0), но у него есть другие представления. Например, мы можем использовать "комбинирующую" кодовую точку с акцентом U+0300 и прикрепить ее к строчной букве a U+0061, чтобы создать тот же символ à. В общем, символ может быть представлен несколькими различными последовательностями кодовых точек и, следовательно, различными последовательностями UTF-8 байтов.

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

"Кодовая точка" - это нечто вроде труднопроизносимого слова, поэтому Go вводит более короткое понятие: руна (rune). Термин появляется в библиотеках и исходном коде и означает то же самое, что и "кодовая точка", с одним интересным дополнением.

Язык Go определяет слово rune как псевдоним для типа int32, поэтому программы могут быть очищены, когда целочисленное значение представляет кодовую точку. Более того, то, что вы можете рассматривать как символьную константу, называется рунной константой (rune constant) в Go. Тип и значение выражения

'⌘'

это руна (rune) с целочисленным значением 0x2318.

Подводя итог, вот основные моменты:

  • Исходный код в Go всегда UTF-8.
  • Строка хранит произвольные байты.
  • Строковый литерал, без экранирования на уровне байтов, всегда содержит допустимые последовательности UTF-8.
  • Эти последовательности представляют кодовые точки Unicode, называемые рунами.
  • В Go нет гарантии, что символы в строках нормализованы.

Range циклы

Помимо аксиоматической детализации, что исходным кодом Go является UTF-8, на самом деле есть только один способ, которым Go обрабатывает UTF-8 специально, а именно при использовании цикла for для строки.

Мы видели, что происходит с обычным циклом for. Цикл for range, напротив, декодирует одну руну в кодировке UTF-8 на каждую итерацию. Каждый раз, когда происходит цикл, индекс цикла - это начальная позиция текущей руны, измеряемая в байтах, а кодовая точка - ее значение. Вот пример, использующий еще один удобный формат Printf, %#U, который показывает значение Unicode кодовой точки и ее печатное представление:

const nihongo = "日本語"
for index, runeValue := range nihongo {
    fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
}

Запустить в песочнице

Выходные данные показывают, как каждая кодовая точка занимает несколько байтов:

U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6

Библиотеки

Стандартная библиотека Go предоставляет мощную поддержку для интерпретации текста UTF-8. Если цикл for range не подходит для ваших целей, скорее всего, средство, которое вам нужно, предоставляется пакетом из библиотеки.

Наиболее важным таким пакетом является unicode/utf8, который содержит вспомогательные процедуры для проверки, дизассемблирования и повторной сборки строк UTF-8. Вот программа, эквивалентная приведенному выше примеру for range, но для выполнения работы используется функция DecodeRuneInString из этого пакета. Возвращаемыми значениями функции являются руна и ее ширина в байтах в кодировке UTF-8.

const nihongo = "日本語"
for i, w := 0, 0; i < len(nihongo); i += w {
    runeValue, width := utf8.DecodeRuneInString(nihongo[i:])
    fmt.Printf("%#U starts at byte position %d\n", runeValue, i)
    w = width
}

Запустить в песочнице

Запустите его, чтобы увидеть, что он выполняет то же самое. Цикл for range и DecodeRuneInString определены для создания точно такой же итерационной последовательности.

Посмотрите документацию по пакету unicode/utf8, чтобы увидеть, какие другие возможности он предоставляет.

Заключение

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

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


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


понедельник, 16 сентября 2019 г.

Сортировка map (карт) по ключу или значению в Golang

Map (карта) в Golang - это неупорядоченная коллекция пар ключ-значение. Порядок записи в map не имеет значения и при итерации пары ключ-значение выдаются в случайном порядке. Если вам нужен стабильный порядок итераций, вы должны поддерживать отдельную структуру данных.

В следующем примере используется отсортированный срез (slice) ключей для печати map[string]int в порядке расположения ключей.

m := map[string]int{"A": 21, "C": 3, "B": 46}

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)

for _, k := range keys {
    fmt.Println(k, m[k])
}

Вывод:

A 21
B 46
C 3

Также, начиная с Go 1.12, пакет fmt печатает карты в порядке сортировки ключей, чтобы упростить тестирование. Например:

m := map[string]int{"A": 21, "C": 3, "B": 46}
fmt.print(m)

Вывод:

map[A:21 B:46 C:3]


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