суббота, 29 февраля 2020 г.

Релиз Go 1.14: основная библиотека

Новый пакет хэширования последовательности байтов

Go 1.14 включает новый пакет hash/maphash, который предоставляет хэш-функции для последовательностей байтов. Эти хэш-функции предназначены для использования для реализации хэш-таблиц или других структур данных, которые должны отображать произвольные строки или байтовые последовательности в равномерное распределение по 64-разрядным целым числам без знака.

Хэш-функции устойчивы к столкновениям (коллизиям), но криптографически небезопасны.

Хэш-значение данной последовательности байтов согласовано в рамках одного процесса, но будет различным в разных процессах.

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

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

crypto/tls

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

TLS 1.3 больше нельзя отключить с помощью переменной среды GODEBUG. Используйте поле Config.MaxVersion для настройки версий TLS.

Когда через поле Config.Certificates предоставляется несколько цепочек сертификатов, автоматически выбирается первая, совместимая с партнером. Это позволяет, например, предоставить ECDSA и сертификат RSA и позволить пакету автоматически выбрать лучший. Обратите внимание, что производительность этого выбора будет низкой, если не установлено поле Certificate.Leaf.

Новые функции CipherSuites и InsecureCipherSuites возвращают список реализованных в настоящее время наборов шифров. Новая функция CipherSuiteName возвращает имя для идентификатора набора шифров.

Новые методы (*ClientHelloInfo).SupportsCertificate и (*CertificateRequestInfo).SupportsCertificate показывают, поддерживает ли одноранговый узел определенный сертификат.

Пакет tls больше не поддерживает устаревшее расширение Next Protocol Negotiation (NPN) и теперь поддерживает только ALPN. В предыдущих релизах он поддерживал оба. Изменений в API нет, и приложения должны работать так же, как и раньше. Большинство других клиентов и серверов уже удалили поддержку NPN в пользу стандартизированной ALPN.

Подписи RSA-PSS теперь используются, когда они поддерживаются в рукопожатиях TLS 1.2. Это не повлияет на большинство приложений, но пользовательские реализации Certificate.PrivateKey, которые не поддерживают подписи RSA-PSS, должны будут использовать новое поле Certificate.SupportedSignatureAlgorithms, чтобы отключить их.

Config.Certificates и Config.GetCertificate теперь могут быть равны nil, если задан Config.GetConfigForClient. Если обратные вызовы не возвращают ни сертификаты, ни ошибку, теперь отправляется unrecognized_name.

Новое поле CertificateRequestInfo.Version предоставляет версию TLS для обратных вызовов сертификатов клиентов.

Новые константы TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 и TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 используют итоговые имена для наборов шифров, ранее называвшихся TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 и TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305.

crypto/x509

Certificate.CreateCRL теперь поддерживает издателей Ed25519.

debug/dwarf

Пакет debug/dwarf теперь поддерживает чтение DWARF версии 5.

Новый метод (*Data).AddSection поддерживает добавление произвольных новых разделов DWARF из входного файла в данные DWARF.

Новый метод (*Reader).ByteOrder возвращает порядок байтов текущей единицы компиляции. Это может использоваться для интерпретации атрибутов, которые закодированы в собственном порядке, таких как описания местоположения.

Новый метод (*LineReader).Files возвращает таблицу имен файлов из программы чтения строк. Это может использоваться для интерпретации значения атрибутов DWARF, таких как AttrDeclFile.

encoding/asn1

Unmarshal теперь поддерживает ASN.1 строковый тип BMPString, представленный новой константой TagBMPString.

encoding/json

Тип Decoder поддерживает новый метод InputOffset, который возвращает смещение байта входного потока текущей позиции декодера.

Compact больше не экранирует символы U+2028 и U+2029, что никогда не было документированной функцией. Для правильного экранирования смотрите HTMLEscape.

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

go/build

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

go/doc

Новая функция NewFromFiles вычисляет документацию пакета из списка *ast.File связанных примеров с соответствующими элементами пакета. Новая информация доступна в новом поле Examples в типах Package, Type и Func, а также в новом поле Suffix в типе Example.

io/ioutil

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

log

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

math

Новая функция FMA вычисляет x*y+z в плавающей точке без промежуточного округления вычисления x*y. Несколько архитектур реализуют это вычисление, используя специальные аппаратные инструкции для дополнительной производительности.

math/big

Метод GCD теперь позволяет входам a и b быть нулевыми или отрицательными.

math/bits

Новые функции Rem, Rem32 и Rem64 поддерживают вычисление остатка, даже если частное переполнено.

mime

Тип файлов .js и .mjs по умолчанию теперь text/javascript, а не application/javascript. Это соответствует проекту IETF, в котором application/javascript рассматривается как устаревшее.

mime/multipart

Новый метод Reader NextRawPart поддерживает выборку следующей части MIME без прозрачного декодирования данных для печати в кавычках.

net/http

Новый метод Values типа Header может использоваться для извлечения всех значений, связанных с канонизированным ключом.

Новое поле DialTLSContext типа Transport можно использовать для указания дополнительной dial функции для создания соединений TLS для не проксируемых запросов HTTPS. Это новое поле можно использовать вместо DialTLS, который сейчас считается устаревшим; DialTLS будет продолжать работать, но новый код должен использовать DialTLSContext, что позволяет Transport отменять dial, как только они больше не нужны.

В Windows ServeFile теперь правильно обслуживает файлы размером более 2 ГБ.

net/http/httptest

Новое поле EnableHTTP2 типа Server поддерживает включение HTTP/2 на тестовом сервере.

net/textproto

Новый метод Values типа MIMEHeader ​​можно использовать для извлечения всех значений, связанных с канонизированным ключом.

os/signal

В Windows события CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT и CTRL_SHUTDOWN_EVENT теперь генерируют сигнал syscall.SIGTERM, аналогично тому, как Control-C и Control-Break генерируют сигнал syscall.SIGINT.

plugin

Пакет plugin теперь поддерживает freebsd/amd64.

reflect

StructOf теперь поддерживает создание типов структур с неэкспортированными полями, устанавливая поле PkgPath в элементе StructField.

runtime

runtime.Goexit больше не может быть прерван рекурсивной panic/recover.

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

runtime/pprof

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

strconv

Тип NumError теперь имеет метод Unwrap, который можно использовать для получения причины сбоя преобразования. Это поддерживает использование значений NumError с ошибками. Чтобы узнать, является ли основной ошибкой strconv.ErrRange или strconv.ErrSyntax.

sync

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

testing

Пакет testing теперь поддерживает функции очистки, вызываемые после завершения теста или бенчмарка, вызывая соответственно T.Cleanup или B.Cleanup.

text/template

Пакет text/template теперь правильно сообщает об ошибках, когда аргумент в скобках используется в качестве функции. Это чаще всего проявляется в ошибочных случаях, таких как {{if (eq .F "a") or (eq .F "b")}}. Это должно быть записано как {{if or (eq .F "a") (eq .F "b")}}. Ошибочный случай никогда не работал, как ожидалось, и теперь будет сообщаться с ошибкой - can't give argument to non-function.

unicode

Пакет unicode и связанная с ним поддержка по всей системе были обновлены с Unicode 11.0 до Unicode 12.0, что добавляет 554 новых символа, включая четыре новых сценария и 61 новый эмодзи.


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


Купить gopher

Релиз Go 1.14: runtime и компилятор

Runtime (среда времени выполнения)

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

Goroutines теперь асинхронно вытесняются. В результате циклы без вызовов функций больше не могут заблокировать планировщик или существенно задержать сборку мусора. Это поддерживается на всех платформах, кроме windows/arm, darwin/arm, js/wasm и plan9/*.

Следствием реализации вытеснения является то, что в системах Unix, включая системы Linux и macOS, программы, собранные (build) с Go 1.14, будут получать больше сигналов, чем программы, собранные (build) с более ранними релизами. Это означает, что программы, использующие такие пакеты, как syscall или golang.org/x/sys/unix, увидят, что более медленные системные вызовы завершаются с ошибками EINTR. Этим программам придется каким-то образом обрабатывать эти ошибки, скорее всего, повторяя цикл, чтобы повторить системный вызов.

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

Внутренние таймеры, используемые time.After, time.Tick, net.Conn.SetDeadline и схожие, более эффективны, с меньшим количеством конфликтов блокировки и меньшим количеством переключений контекста. Это улучшение производительности, которое не должно вызывать видимых изменений.

Компилятор

В релизе Go 1.14 добавлена ​​опция -d=checkptr в качестве параметра времени компиляции для добавления инструментария для проверки того, что код Go соответствует unsafe.Pointer правилам безопасности динамически. Эта опция включается по умолчанию (кроме Windows) с флагами -race или -msan и может быть отключена с -gcflags=all=-d=checkptr=0. В частности, -d=checkptr проверяет следующее:

  1. При преобразовании unsafe.Pointer в *T результирующий указатель должен быть выровнен соответствующим образом для T.
  2. Если результат арифметики указателя указывает на объект кучи (heap) Go, один из операндов с типом unsafe.Pointer должен указывать на тот же объект.

Использование -d=checkptr в настоящее время не рекомендуется в Windows, потому что это вызывает ложные предупреждения в стандартной библиотеке.

Компилятор теперь может генерировать машиночитаемые журналы ключевых оптимизаций, используя флаг -json, включая встраивание (inlining), escape-анализ, исключение bounds-check (исключение проверки границ) и nil-check.

Детальный escape-анализ (-m=2) теперь снова работает. Ранее он был исключен из новой реализации escape-анализа в предыдущем релизе.

Все символы Go в двоичных файлах macOS теперь начинаются с подчеркивания, следуя соглашениям платформы.

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

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


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


Купить gopher

Релиз Go 1.14: инструменты, команда go

Вендоринг

Когда основной модуль содержит каталог vendor верхнего уровня и его файл go.mod указывает go 1.14 или выше, команда go по умолчанию теперь имеет значение -mod=vendor для операций, которые принимают этот флаг. Новое значение для этого флага, -mod=mod, заставляет команду go вместо этого загружать модули из кэша модулей (например, когда отсутствует каталог vendor).

Если задано -mod=vendor (явно или по умолчанию), команда go теперь проверяет, что файл vendor/modules.txt основного модуля соответствует его файлу go.mod.

go list -m больше безмолвно не пропускает транзитивные зависимости, которые не предоставляют пакеты в каталоге vendor. Теперь он заврешается неудачей и выдает явную ошибку, если установлен -mod=vendor и запрашивается информация для модуля, не упомянутого в vendor/modules.txt.

Флаги

Команда go get больше не принимает флаг -mod. Ранее настройка флага либо игнорировалась, либо приводила к сбою сборки.

-mod=readonly теперь устанавливается по умолчанию, когда файл go.mod доступен только для чтения и отсутствует каталог vendor верхнего уровня.

-modcacherw - это новый флаг, который указывает команде go оставлять вновь созданные каталоги в кеше модуля с их разрешениями по умолчанию, а не делать их доступными только для чтения. Использование этого флага повышает вероятность того, что тесты или другие инструменты случайно добавят файлы, не включенные в проверенную контрольную сумму модуля. Однако он позволяет использовать rm -rf (вместо go clean -modcache) для удаления кэша модуля.

-modfile=file - это новый флаг, который указывает команде go читать (и, возможно, записывать) альтернативный файл go.mod вместо файла в корневом каталоге модуля. Файл с именем go.mod должен все еще присутствовать для определения корневого каталога модуля, но к нему нет доступа. Если указан параметр -modfile, также используется альтернативный файл go.sum: его путь определяется из флага -modfile путем обрезки расширения .mod и добавления .sum.

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

GOINSECURE - это новая переменная среды, которая указывает команде go не требовать HTTPS-соединения и пропускать проверку сертификата при извлечении определенных модулей непосредственно из их источников. Как и существующая переменная GOPRIVATE, значение GOINSECURE представляет собой список glob паттернов, разделенных запятыми.

Команды вне модулей

Когда режим поддержки модуля включен явно (путем установки GO111MODULE=on), большинство команд модуля имеют более ограниченную функциональность, если отсутствует файл go.mod. Например, go build, go run, а другие команды сборки могут собирать пакеты только в стандартной библиотеке и пакеты, указанные как файлы .go в командной строке.

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

go get продолжает работать так же, как и раньше, так же как и go mod download и go list -m с явными версиями.

+incompatible версии

Если последняя версия модуля содержит файл go.mod, go get больше не будет обновляться до несовместимой (incompatible) основной версии этого модуля, если только такая версия не запрашивается явно или уже требуется. go list также пропускает несовместимые основные версии для такого модуля при извлечении непосредственно из системы управления версиями, но может включать их, если о них сообщает прокси.

Обслуживание файла go.mod

Команды go, кроме go mod tidy, больше не удаляют директиву require, которая указывает версию косвенной зависимости, которая уже подразумевается другими (транзитивными) зависимостями основного модуля.

Команды go, кроме go mod tidy, больше не редактируют файл go.mod, если изменения носят только косметический характер.

Когда задано -mod=readonly, команды go больше не будут работать из-за отсутствующей директивы go или ошибочного // indirect комментария.

Загрузка модуля

Команда go теперь поддерживает хранилища Subversion в режиме модуля.

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

Тестирование

go test -v теперь передает поток t.Log тогда, когда это происходит, а не в конце всех тестов.


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


Купить gopher

Релиз Go 1.14: изменения в языке и его портах

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

Поддержка модулей (modules) в команде go теперь готова к производственному использованию, и всем пользователям рекомендуется перейти на модули Go для управления зависимостями.

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

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

Порты


Darwin

Go 1.14 - последний релиз, который будет работать на MacOS 10.11 El Capitan. Go 1.15 потребует macOS 10.12 Sierra или более поздней версии.

Go 1.14 - последний релиз Go, поддерживающий 32-битные двоичные файлы в macOS (порт darwin/386). Они больше не поддерживаются macOS, начиная с macOS 10.15 (Catalina). Go продолжает поддерживать 64-битный порт darwin/amd64.

Go 1.14, вероятно, будет последним выпуском Go, поддерживающим 32-разрядные двоичные файлы на iOS, iPadOS, watchOS и tvOS (порт darwin/arm). Go продолжает поддерживать 64-битный порт darwin/arm64.

Windows

В двоичных файлах Windows теперь включена функция DEP (предотвращение выполнения данных).

WebAssembly

Значения JavaScript, на которые ссылаются из Go через объекты js.Value, теперь могут собираться сборщиком мусора.

Значения js.Value больше нельзя сравнивать с помощью оператора ==, и вместо этого их нужно сравнивать с помощью их метода Equal.

В js.Value теперь есть методы IsUndefined, IsNull и IsNaN.

RISC-V

В версии 1.14 содержится экспериментальная поддержка 64-разрядной версии RISC-V в Linux (GOOS = linux, GOARCH = riscv64). Помните, что производительность, стабильность синтаксиса сборки и, возможно, правильность находятся в стадии разработки.

FreeBSD

Go теперь поддерживает 64-битную архитектуру ARM на FreeBSD 12.0 или более поздней версии (порт freebsd/arm64).

Native Client (NaCl)

Как было объявлено в примечаниях к выпуску Go 1.13, Go 1.14 прекращает поддержку платформы Native Client (GOOS = nacl).

Illumos

Теперь среда выполнения учитывает ограничения CPU зоны (элемент управления ресурсами zone.cpu-cap) для runtime.NumCPU и значение по умолчанию GOMAXPROCS.


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


Купить gopher

пятница, 28 февраля 2020 г.

Три способа сравнивания срезов (массивов) в Golang

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

// Equal проверяет, что a и b содержат одинаковые элементы.
// nil аргумент эквивалентен пустому срезу.
func Equal(a, b []int) bool {
    if len(a) != len(b) {
        return false
    }
    for i, v := range a {
        if v != b[i] {
            return false
        }
    }
    return true
}

Однако для массивов вы можете использовать операторы сравнения == и !=.

a := [2]int{1, 2}
b := [2]int{1, 3}
fmt.Println(a == b) // false

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

Оптимизированный код для байтовых срезов

Чтобы сравнить байтовые срезы, используйте оптимизированные bytes.Equal. Эта функция также обрабатывает nil аргументы как эквивалент пустых срезов.

Универсальный код для рекурсивного сравнения

В целях тестирования вы можете использовать reflect.DeepEqual. Он сравнивает два элемента любого типа рекурсивно.

var a []int = nil
var b []int = make([]int, 0)
fmt.Println(reflect.DeepEqual(a, b)) // false

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


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


Купить gopher

четверг, 27 февраля 2020 г.

Пакеты в Golang: объявление, импорт, загрузка, документирование

Каждая программа Go состоит из пакетов, и каждый пакет имеет путь импорта:

"fmt"
"math/rand"
"github.com/yourbasic/graph"

Пакеты в стандартной библиотеке имеют короткие пути импорта, такие как "fmt" и "math/rand". Сторонние пакеты, такие как "github.com/yourbasic/graph", обычно имеют путь импорта, который включает в себя хостинг (github.com) и название организации (yourbasic).

По соглашению имя пакета совпадает с последним элементом пути импорта:

fmt
rand
yourbasic

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

package main

import (
    "fmt"
    "math/rand"

    "github.com/yourbasic/graph"
)

func main() {
    n := rand.Intn(100)
    g := graph.New(n)
    fmt.Println(g)
}

Объявление пакета

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

Например, файл src/math/rand/exp.go, являющийся частью реализации пакета math/rand, содержит следующий код.

package rand
  
import "math"
  
const re = 7.69711747013104972
…

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

Конфликты имен пакетов

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

package main

import (
    csprng "crypto/rand"
    prng "math/rand"

    "fmt"
)

func main() {
    n := prng.Int() // псевдослучайное число
    b := make([]byte, 8)
    // криптографически безопасное псевдослучайное число
    csprng.Read(b) 
    fmt.Println(n, b)
}

Точки в импорте

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

package main

import (
    "fmt"
    . "math"
)

func main() {
    fmt.Println(Sin(Pi/2)*Sin(Pi/2) + Cos(Pi)/2) // 0.5
}

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

Загрузка пакета

Команда go get загружает пакеты, названные путями импорта, вместе с их зависимостями, а затем устанавливает пакеты.

$ go get github.com/go-sql-driver/mysql

Путь импорта соответствует хранилищу кода. Это уменьшает вероятность будущих конфликтов имен.

Пакетная документация

На веб-сайте GoDoc размещена документация для всех общедоступных пакетов Go на Bitbucket, GitHub, Google Project Hosting и Launchpad:

https://godoc.org/fmt
https://godoc.org/math/rand
https://godoc.org/github.com/go-sql-driver/mysql

Команда godoc извлекает и генерирует документацию для всех локально установленных программ Go. Следующая команда запускает веб-сервер, который представляет документацию по адресу http://localhost:6060/.

$ godoc -http=:6060


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


Купить gopher

среда, 26 февраля 2020 г.

Пять паттернов использования операторов switch

Базовый switch с default кейсом

  • Оператор switch запускает первый case (кейс), равный выражению условия.
  • Кейсы оцениваются сверху вниз, останавливаясь, когда кейс подходит.
  • Если ни один кейс не совпадает и есть default кейс, выполняются его утверждения.

switch time.Now().Weekday() {
case time.Saturday:
    fmt.Println("Сегодня суббота.")
case time.Sunday:
    fmt.Println("Сегодня воскресенье.")
default:
    fmt.Println("Сегодня будничный день.")
}

В отличие от C и Java, выражения case не обязательно должны быть константами.

Без выражения условия

Switch без условия аналогичен switch true.

// пропущенное выражение условия означает "true"
switch hour := time.Now().Hour(); { 
case hour < 12:
    fmt.Println("Доброе утро!")
case hour < 17:
    fmt.Println("Добрый день!")
default:
    fmt.Println("Добрый вечер!")
}

Кейс - список

В Go в качестве кейса можно использовать список значений - если выражение условия совпадает с одним из элементов списка, тогда кейс считается совпавшим:

func WhiteSpace(c rune) bool {
    switch c {
    case ' ', '\t', '\n', '\f', '\r':
        return true
    }
    return false
}

Fallthrough (проваливание в следующий кейс)

Оператор fallthrough передает управление следующему кейсу.

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

switch 2 {
case 1:
    fmt.Println("1")
    fallthrough
case 2:
    fmt.Println("2")
    fallthrough
case 3:
    fmt.Println("3")
}

Вывод:

2
3

Выход с break

Оператор break завершает выполнение самого внутреннего оператора for, switch или select.

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

Loop:
    for _, ch := range "a b\nc" {
        switch ch {
        case ' ': // пропускаем пробел
            break
        case '\n': // break на newline символе
            break Loop
        default:
            fmt.Printf("%c\n", ch)
        }
    }

Вывод:

a
b

На символе a выполняется default кейс. На пробеле оператор break производит выход из switch. Далее на символе b выполняется default кейс. Далее на символе новой строки (newline - \n) происходит выход к метке Loop и выход из for range цикла.

Порядок исполнения

  • Сначала выражение switch оценивается один раз.
  • Затем case выражения оцениваются слева направо и сверху вниз:
    • первое, равное выражению switch, запускает выполнение операторов соответствующего case,
    • другие case пропущены.

// Foo печатает и возвращает n.
func Foo(n int) int {
    fmt.Println(n)
    return n
}

func main() {
    switch Foo(2) {
    case Foo(1), Foo(2), Foo(3):
        fmt.Println("Первый case")
        fallthrough
    case Foo(4):
        fmt.Println("Второй case")
    }
}

Вывод:

2
1
2
Первый case
Второй case


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


вторник, 25 февраля 2020 г.

Пять базовых паттернов использования for циклов

Оператор for используется для многократного выполнения блока кода.

Трехкомпонентная цикл

Эта версия for цикла в Go работает так же, как в C или Java.

sum := 0
for i := 1; i < 5; i++ {
    sum += i
}
fmt.Println(sum) // 10 (1+2+3+4)

  1. Выполняется инициализирующее утверждение, i := 1
  2. Вычисляется условие, i < 5
    • Если true, тело цикла выполняется,
    • в противном случае цикл завершен.
  3. Выполняется постутверждение i++
  4. Вернуться к шагу 2.

Область видимости i ограничена циклом.

While цикл

Если вы пропустите инициализирующее и постутверждение, вы получите цикл while.

n := 1
for n < 5 {
    n *= 2
}
fmt.Println(n) // 8 (1*2*2*2)

  1. Вычисляется условие, i < 5
    • Если true, тело цикла выполняется,
    • в противном случае цикл завершен.
  2. Вернуться к шагу 1.

Бесконечный цикл

Если вы пропустите условие, вы получите бесконечный цикл.

sum := 0
for {
    sum++ // повторяется бесконечно
}
fmt.Println(sum) // никогда не будет выполнено

For-each range цикл

Прохождение в цикле по элементам в срезах, массивах, картах, каналах или строках часто лучше сделать с помощью range цикла.

strings := []string{"hello", "world"}
for i, s := range strings {
    fmt.Println(i, s)
}

Вывод:

0 hello
1 world

Выход из цикла

Ключевые слова break и continue работают так же, как и в C и Java.

sum := 0
for i := 1; i < 5; i++ {
    if i%2 != 0 { // пропустить нечетные числа
        continue
    }
    sum += i
}
fmt.Println(sum) // 6 (2+4)

  • Оператор continue начинает следующую итерацию самого внутреннего цикла for, выполнив свое постутверждение (i++).
  • Оператор break покидает самое внутреннее for, switch, select утверждение.

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


суббота, 22 февраля 2020 г.

Карта (map) в Golang: создание, добавление, получение, удаление

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

Создание новой карты

var m map[string]int                // nil карта 
                                    // string-int пар

m1 := make(map[string]float64)      // пустая карта 
                                    // string-float64 пар

m2 := make(map[string]float64, 100) // преаллоцируем место 
                                    // для 100 вхождений

m3 := map[string]float64{           // литерал карты
    "e":  2.71828,
    "pi": 3.1416,
}
fmt.Println(len(m3))                // размер карты: 2

  • Карта (или словарь) - это неупорядоченная коллекция пар ключ-значение, где каждый ключ уникален.
  • Вы создаете новую карту с оператором make или литералом карты.
  • Нулевым значением по умолчанию для карты является nil. nil карта эквивалентна пустой карте, за исключением того, что элементы не могут быть добавлены.
  • Функция len возвращает размер карты, который представляет собой количество пар ключ-значение.

Предупреждение: если вы попытаетесь добавить элемент в неинициализированную карту, вы получите загадочную ошибку времени выполнения (run-time error) Присвоение к вхождению в nil-карте (assignment to entry in nil map).

Добавление, обновление, получение, удаление ключа/значения

m := make(map[string]float64)

m["pi"] = 3.14             // Добавляем новую пару 
                           // ключ-значение
m["pi"] = 3.1416           // Обновляем значение
fmt.Println(m)             // Печатаем карту: 
                           // "map[pi:3.1416]"

v := m["pi"]               // Получаем значение: 
                           // v == 3.1416
v = m["pie"]               // Не найдено: 
                           // v == 0 (нулевое значение)

_, found := m["pi"]        // found == true
_, found = m["pie"]        // found == false

if x, found := m["pi"]; found {
    fmt.Println(x)
}                           // Печатает "3.1416"

delete(m, "pi")             // Удаляем пару ключ-значение
fmt.Println(m)              // Печатаем карту: "map[]"

  • Когда вы индексируете карту, вы получаете два возвращаемых значения; второе (необязательное) является логическим значением, указывающим, существует ли ключ.
  • Если ключ не существует, первое значение будет нулевым значением по умолчанию.

For-each range цикл

m := map[string]float64{
    "pi": 3.1416,
    "e":  2.71828,
}
fmt.Println(m) // "map[e:2.71828 pi:3.1416]"

for key, value := range m { // Порядок не определен 
    fmt.Println(key, value)
}

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

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

Производительность и реализация

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

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


четверг, 20 февраля 2020 г.

Две базовых реализации очереди FIFO в Golang

Простой способ реализовать структуру данных временной очереди в Go - это использовать срез:

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

var queue []string

queue = append(queue, "Hello ") // Добавление в очередь
queue = append(queue, "world!")

for len(queue) > 0 {
    fmt.Print(queue[0]) // Первый элемент
    queue = queue[1:]   // Удаление из очереди
}

Вывод:

Hello world!

Остерегайтесь утечек памяти

Вы можете удалить первый элемент перед удалением из очереди.

// Удаление из очереди
queue[0] = "" // Удаляем элемент 
              // (записываем нулевое значение)
queue = queue[1:]

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

Связанный список

Пакет container/list реализует двусвязный список, который можно использовать в качестве очереди.

queue := list.New()

queue.PushBack("Hello ") // Добавление в очередь
queue.PushBack("world!")

for queue.Len() > 0 {
    e := queue.Front() // Первый элемент
    fmt.Print(e.Value)

    queue.Remove(e) // Удаление из очереди
}

Вывод:

Hello world!


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


Структура данных базового стека (LIFO) в Golang

Идиоматический способ реализации структуры данных стека в Go состоит в использовании среза:

  • для push (добавления элемента в стек) вы используете встроенную функцию append
  • для pop (извлечения верхнего элемента стека) вы выполняете срезание верхнего элемента

var stack []string

stack = append(stack, "world!") // Push
stack = append(stack, "Hello ")

for len(stack) > 0 {
    n := len(stack) - 1 // Верхний элемент
    fmt.Print(stack[n])

    stack = stack[:n] // Pop
}

Вывод:

Hello world!

Производительность

Добавление отдельного элемента к срезу требует постоянного амортизированного времени.

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

// Pop
stack[n] = "" // Удаляем элемент (записываем нулевое значение)
stack = stack[:n]

О том что такое стек рекомендуем эту статью.


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


вторник, 18 февраля 2020 г.

Срезы и массивы в Golang: создание, индексирование, нарезка, итерация

Срез не хранит никаких данных, он просто описывает часть базового массива.

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

Срез может расти и уменьшаться в пределах основного массива.

Срезы индексируются обычным способом: s[i] обращается к i-му элементу, начиная с нуля.

Создание

var s []int                   // nil срез
s1 := []string{"foo", "bar"}
s2 := make([]int, 2)          // то же что и []int{0, 0}
s3 := make([]int, 2, 4)       // то же что и new([4]int)[:2]
fmt.Println(len(s3), cap(s3)) // 2 4

Нулевым значением по умолчанию для среза является nil. Функции len, cap и append все рассматривают nil как пустой срез с нулевой емкостью.

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

Встроенные функции len и cap определяют длину и емкость.

Нарезка

a := [...]int{0, 1, 2, 3} // массив
s := a[1:3]               // s == []int{1, 2}        cap(s) == 3
s = a[:2]                 // s == []int{0, 1}        cap(s) == 4
s = a[2:]                 // s == []int{2, 3}        cap(s) == 2
s = a[:]                  // s == []int{0, 1, 2, 3}  cap(s) == 4

Вы также можете создать срез, разрезая существующий массив или срез.

Срез формируется путем указания нижней границы и верхней границы: a[low:high]. Эта конструкция выбирает полуоткрытый диапазон, который включает первый элемент, но исключает последний.

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

s := []int{0, 1, 2, 3, 4} // срез
s = s[1:4]                // s == []int{1, 2, 3}
s = s[1:2]                // s == []int{2} (индексирование относительно среза)
s = s[:3]                 // s == []int{2, 3, 4} (расширение длины)

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

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

Попытка выйти за пределы возможностей вызывает панику.

Итерация

s := []string{"Foo", "Bar"}
for i, v := range s {
    fmt.Println(i, v)
}

Вывод:

0 Foo
1 Bar

Выражение диапазона s вычисляется один раз перед началом цикла.

Значения итерации присваиваются соответствующим переменным итерации, i и v, как в операторе присваивания.

Вторая итерационная переменная является необязательной.

Если срез равен nil, количество итераций равно 0.

Append и copy

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

Функция copy копирует элементы в целевой срез dst из исходного среза src. Количество копируемых элементов - это минимум len(dst) и len(src).

Стеки и очереди

Идиоматический способ реализации стека или очереди в Go - это непосредственное использование среза.


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


Почему nil может быть не равен nil в Golang

Почему в этом примере nil не равен nil?

func Foo() error {
    var err *os.PathError = nil
    // …
    return err
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // false
}

Ответ

Значение интерфейса равно nil, только если его значение и динамический тип равны nil. В приведенном выше примере Foo() возвращает [nil, *os.PathError], и мы сравниваем его с [nil, nil].

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

…
fmt.Println(err == (*os.PathError)(nil)) // true
…

Лучший подход

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

func Foo() (err error) {
    // …
    // err не назначен и имеет нулевое значение [nil, nil]
    return 
}

func main() {
    err := Foo()
    fmt.Println(err)        // <nil>
    fmt.Println(err == nil) // true
}

Рекомендация. Используйте встроенный тип интерфейса error, а не конкретный тип, для хранения и возврата значений ошибок.


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


понедельник, 17 февраля 2020 г.

Создание, инициализация и сравнение структур в Golang

Структура (struct) - это типизированная коллекция полей, полезная для группировки данных в записи.

type Student struct {
    Name string
    Age  int
}

var a Student    // a == Student{"", 0}
a.Name = "Ivan"  // a == Student{"Ivan", 0}

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

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

Вы можете получить доступ к отдельным полям с точечной нотацией.

Два способа создания и инициализации новой структуры

Ключевое слово new может быть использовано для создания новой структуры. Возвращает указатель на вновь созданную структуру.

var pa *Student   // pa == nil
pa = new(Student) // pa == &Student{"", 0}
pa.Name = "Ivan"  // pa == &Student{"Ivan", 0}

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

b := Student{ // b == Student{"Anton", 0}
    Name: "Anton",
}
    
pb := &Student{ // pb == &Student{"Anton", 8}
    Name: "Anton",
    Age:  8,
}

c := Student{"Kate", 5}    // c == Student{"Kate", 5}
d := Student{}             // d == Student{"", 0}

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

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

Литерал может опускать список элементов; такой литерал дает нулевое значение для своего типа.

Сравненение структур

Вы можете сравнивать значения структуры с операторами сравнения == и !=. Два значения равны, если их соответствующие поля равны.

d1 := Student{"Albert", 1}
d2 := Student{"Albert", 2}
fmt.Println(d1 == d2) // false


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


суббота, 15 февраля 2020 г.

Массивы, срезы и строки: механика работы append в Golang

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

  • фиксированный размер или переменный размер?
  • размер является частью типа?
  • как выглядят многомерные массивы?
  • имеет ли пустой массив значение?

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

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

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

Массивы

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

Массивы не часто встречаются в программах Go, потому что размер массива является частью его типа, что ограничивает его выразительность.

Декларация

var buffer [256]byte

объявляет переменную buffer, которая содержит 256 байтов. Тип buffer включает его размер, [256]byte. Массив с 512 байтами будет иметь другой тип - [512]byte.

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

buffer: byte byte byte ... 256 раз ... byte byte byte

То есть переменная содержит 256 байтов данных и ничего больше. Мы можем получить доступ к его элементам с помощью знакомого синтаксиса индексации, buffer[0], buffer[1] и т. д. до buffer[255]. (Диапазон индекса от 0 до 255 охватывает 256 элементов.) Попытка индексировать буфер со значением вне этого диапазона приведет к сбою программы.

Существует встроенная функция len, которая возвращает количество элементов массива или среза, а также несколько других типов данных. Для массивов очевидно, что возвращает len. В нашем примере len(buffer) возвращает фиксированное значение 256.

Массивы имеют свое место - они, например, являются хорошим представлением матрицы преобразования - но их наиболее распространенная цель в Go - хранить память для среза.

Срезы: заголовок среза (slice header)

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

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

Учитывая нашу переменную массива buffer из предыдущего раздела, мы могли бы создать срез, который описывает элементы со 100 по 150 (точнее, со 100 по 149 включительно), разрезая массив:

var slice []byte = buffer[100:150]

В этом примере мы использовали полное объявление переменной, чтобы быть явными. Переменная slice имеет тип []byte, произносится как "срез байтов" и инициализируется из массива, называемого buffer, путем разбиения элементов от 100 (включительно) до 150 (исключая). Более идиоматический синтаксис отбрасывает тип, который устанавливается инициализирующим выражением:

var slice = buffer[100:150]

Внутри функции мы можем использовать краткую форму объявления,

slice := buffer[100:150]

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

type sliceHeader struct {
    Length        int
    ZerothElement *byte
}

slice := sliceHeader{
    Length:        50,
    ZerothElement: &buffer[100],
}

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

До сих пор мы использовали операцию среза в массиве, но мы также можем нарезать срез, например так:

slice2 := slice[5:10]

Как и прежде, эта операция создает новый срез, в данном случае с элементами с 5 по 9 (включительно) исходного среза, что означает элементы с 105 по 109 исходного массива. Базовая структура sliceHeader для переменной slice2 выглядит следующим образом:

slice2 := sliceHeader{
    Length:        5,
    ZerothElement: &buffer[105],
}

Обратите внимание, что этот header по-прежнему указывает на тот же базовый массив, хранящийся в переменной buffer.

Мы также можем создать срез заново, то есть срезать срез и сохранить результат обратно в исходную структуру slice. После

slice = slice[5:10]

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

slice = slice[1:len(slice)-1]

Вы часто будете слышать, как опытные программисты на Go говорят о "slice header" (заголовок среза), потому что это именно то, что хранится в переменной среза. Например, когда вы вызываете функцию, которая принимает срез в качестве аргумента, например bytes.IndexRune, этот заголовок - это то, что передается в функцию. В этом вызове

slashPos := bytes.IndexRune(slice, '/')

аргумент slice, который передается в функцию IndexRune, фактически является "slice header".

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

Передача срезов в функции

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

Это имеет значение.

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

Рассмотрим эту простую функцию:

func AddOneToEachElement(slice []byte) {
    for i := range slice {
        slice[i]++
    }
}

Он делает то, что подразумевает его имя, перебирая индексы среза (используя цикл for), увеличивая его элементы.

Выполним:

func main() {
    slice := buffer[10:20]
    for i := 0; i < len(slice); i++ {
        slice[i] = byte(i)
    }
    fmt.Println("before", slice)
    AddOneToEachElement(slice)
    fmt.Println("after", slice)
}

Вывод:

before [0 1 2 3 4 5 6 7 8 9]
after [1 2 3 4 5 6 7 8 9 10]

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

Аргумент функции действительно является копией, как показано в следующем примере:

func SubtractOneFromLength(slice []byte) []byte {
    slice = slice[0 : len(slice)-1]
    return slice
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    newSlice := SubtractOneFromLength(slice)
    fmt.Println("After:  len(slice) =", len(slice))
    fmt.Println("After:  len(newSlice) =", len(newSlice))
}

Вывод:

Before: len(slice) = 50
After:  len(slice) = 50
After:  len(newSlice) = 49

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

Указатели на срезы: получатели методов

Другой способ заставить функцию изменить заголовок среза - передать на него указатель. Вот вариант нашего предыдущего примера, который делает это:

func PtrSubtractOneFromLength(slicePtr *[]byte) {
    slice := *slicePtr
    *slicePtr = slice[0 : len(slice)-1]
}

func main() {
    fmt.Println("Before: len(slice) =", len(slice))
    PtrSubtractOneFromLength(&slice)
    fmt.Println("After:  len(slice) =", len(slice))
}

Вывод:

Before: len(slice) = 50
After:  len(slice) = 49

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

Допустим, мы хотели иметь метод на срезе, который усекает его на последнем слэше. Мы могли бы написать это так:

type path []byte

func (p *path) TruncateAtFinalSlash() {
    i := bytes.LastIndex(*p, []byte("/"))
    if i >= 0 {
        *p = (*p)[0:i]
    }
}

func main() {
    // Преобразование из строки в путь.
    pathName := path("/usr/bin/tso") 
    pathName.TruncateAtFinalSlash()
    fmt.Printf("%s\n", pathName)
}

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

Вывод:

/usr/bin

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

type path []byte

func (p path) ToUpper() {
    for i, b := range p {
        if 'a' <= b && b <= 'z' {
            p[i] = b + 'A' - 'a'
        }
    }
}

func main() {
    pathName := path("/usr/bin/tso")
    pathName.ToUpper()
    fmt.Printf("%s\n", pathName)
}

Вывод:

/USR/BIN/TSO

Здесь метод ToUpper использует две переменные в конструкции for range для захвата элемента index и slice. Эта форма цикла позволяет избежать записи p[i] несколько раз в теле.

Емкость (capacity)

Посмотрите на следующую функцию, которая расширяет свой аргумент slice срез целых чисел на один элемент:

func Extend(slice []int, element int) []int {
    n := len(slice)
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

Теперь выполним

func main() {
    var iBuffer [10]int
    slice := iBuffer[0:0]
    for i := 0; i < 20; i++ {
        slice = Extend(slice, i)
        fmt.Println(slice)
    }
}

Вывод:

[0]
[0 1]
[0 1 2]
[0 1 2 3]
[0 1 2 3 4]
[0 1 2 3 4 5]
[0 1 2 3 4 5 6]
[0 1 2 3 4 5 6 7]
[0 1 2 3 4 5 6 7 8]
[0 1 2 3 4 5 6 7 8 9]
panic: runtime error: slice bounds out of range [:11] with capacity 10

Посмотрите, как срез растет, пока ... это не так.

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

type sliceHeader struct {
    Length        int
    Capacity      int
    ZerothElement *byte
}

Поле Capacity записывает, сколько места на самом деле имеет базовый массив; это максимальное значение, которое может достигать длина. Попытка вырастить срез за пределы его емкости выйдет за пределы массива и вызовет panic.

После того, как наш пример slice создан

slice := iBuffer[0:0]

его заголовок выглядит так:

slice := sliceHeader{
    Length:        0,
    Capacity:      10,
    ZerothElement: &iBuffer[0],
}

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

if cap(slice) == len(slice) {
    fmt.Println("slice is full!")
}

Встроенная функция make

Что если мы хотим вырастить срез сверх его возможностей? Это невозможно! По определению, емкость - это предел роста. Но вы можете получить эквивалентный результат, выделив новый массив, скопировав данные и изменив срез, чтобы описать новый массив.

Начнем с выделения. Мы могли бы использовать встроенную функцию new, чтобы выделить больший массив и затем нарезать результат, но вместо этого проще использовать встроенную функцию make. Она выделяет новый массив и создает заголовок среза, чтобы описать его, все сразу. Функция make принимает три аргумента: тип среза, его начальную длину и его емкость, то есть длину массива, который make выделяет для хранения данных среза. Этот вызов создает фрагмент длиной 10 с местом для еще 5 (15-10), как вы можете увидеть, запустив его:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

Вывод:

len: 10, cap: 15

Этот пример удваивает емкость нашего int среза, но сохраняет его длину такой же:

slice := make([]int, 10, 15)
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
newSlice := make([]int, len(slice), 2*cap(slice))
for i := range slice {
    newSlice[i] = slice[i]
}
slice = newSlice
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))

Вывод:

len: 10, cap: 15
len: 10, cap: 30

После запуска этого кода у среза будет гораздо больше места для роста, прежде чем потребуется другое перераспределение.

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

gophers := make([]Gopher, 10)

срез gophers имеет длину и емкость 10.

Встроенная функция copy

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

newSlice := make([]int, len(slice), 2*cap(slice))
copy(newSlice, slice)

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

Функция copy также делает все правильно, когда источник и пункт назначения перекрываются, что означает, что ее можно использовать для перемещения элементов в одном срезе. Вот как использовать copy для вставки значения в середину среза.

// Insert вставляет значение в срез по указанному индексу,
// который должен быть в диапазоне.
// Срез должен иметь место для нового элемента.
func Insert(slice []int, index, value int) []int {
    // Вырастить срез на один элемент.
    slice = slice[0 : len(slice)+1]
    // Используем copy, чтобы переместить 
    // верхнюю часть среза в сторону и открыть отверстие.
    copy(slice[index+1:], slice[index:])
    // Сохраняем новое значение.
    slice[index] = value
    // Возвращаем результат.
    return slice
}

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

slice[i:]

означает тоже самое что и

slice[i:len(slice)]

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

slice[:]

просто означает сам срез, который полезен при разрезании массива. Это выражение является кратчайшим способом сказать "срез, описывающий все элементы массива":

array[:]

Теперь это не так, давайте запустим нашу функцию Insert.

// Отметьте емкость > длины: 
// пространство для добавления элемента.
slice := make([]int, 10, 20) 
for i := range slice {
    slice[i] = i
}
fmt.Println(slice)
slice = Insert(slice, 5, 99)
fmt.Println(slice)

Вывод:

[0 1 2 3 4 5 6 7 8 9]
[0 1 2 3 4 99 5 6 7 8 9]

Append: пример

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

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // Срез полон; должен расти.
        // Мы удваиваем его размер и добавляем 1, 
        // поэтому, если размер равен нулю, 
        // мы все равно наращиваем.
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

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

slice := make([]int, 0, 5)
for i := 0; i < 10; i++ {
    slice = Extend(slice, i)
    fmt.Printf("len=%d cap=%d slice=%v\n", len(slice), cap(slice), slice)
    fmt.Println("address of 0th element:", &slice[0])
}

Вывод:

len=1 cap=5 slice=[0]
address of 0th element: 0x456000
len=2 cap=5 slice=[0 1]
address of 0th element: 0x456000
len=3 cap=5 slice=[0 1 2]
address of 0th element: 0x456000
len=4 cap=5 slice=[0 1 2 3]
address of 0th element: 0x456000
len=5 cap=5 slice=[0 1 2 3 4]
address of 0th element: 0x456000
len=6 cap=11 slice=[0 1 2 3 4 5]
address of 0th element: 0x454030
len=7 cap=11 slice=[0 1 2 3 4 5 6]
address of 0th element: 0x454030
len=8 cap=11 slice=[0 1 2 3 4 5 6 7]
address of 0th element: 0x454030
len=9 cap=11 slice=[0 1 2 3 4 5 6 7 8]
address of 0th element: 0x454030
len=10 cap=11 slice=[0 1 2 3 4 5 6 7 8 9]
address of 0th element: 0x454030

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

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

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

func Append(slice []int, items ...int) []int

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

// Append добавляет элементы к срезу.
// Первая версия: просто вызов Extend в цикле.
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

Обратите внимание на цикл for, проходящий по элементам аргумента items, который подразумевает тип []int. Также обратите внимание на использование пустого идентификатора _ для отбрасывания индекса в цикле, который нам не нужен в этом случае.

Запустим:

slice := []int{0, 1, 2, 3, 4}
fmt.Println(slice)
slice = Append(slice, 5, 6, 7, 8)
fmt.Println(slice)

Вывод:

[0 1 2 3 4]
[0 1 2 3 4 5 6 7 8]

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

slice := []int{0, 1, 2, 3, 4}

Функция Append интересна по другой причине. Мы можем не только добавлять элементы, мы можем добавить целый второй срез, "разорвав" его на аргументы, используя обозначение ... на месте вызова:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // '...' важно!
fmt.Println(slice1)

Вывод:

[0 1 2 3 4]
[0 1 2 3 4 55 66 77]

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

// Append добавляет элементы к срезу.
// Эффективная версия.
func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
        // Перераспределяем. 
        // Выращиваем в 1,5 раза новый размер, 
        // чтобы мы могли расти.
        newSize := total*3/2 + 1
        newSlice := make([]int, total, newSize)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}

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

Выполним; поведение такое же, как и раньше:

slice1 := []int{0, 1, 2, 3, 4}
slice2 := []int{55, 66, 77}
fmt.Println(slice1)
slice1 = Append(slice1, slice2...) // '...' важно!
fmt.Println(slice1)

Вывод:

[0 1 2 3 4]
[0 1 2 3 4 55 66 77]

Append: встроенная функция

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

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

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

Вот примеры использования append:

// Создаем пару новых срезов.
slice := []int{1, 2, 3}
slice2 := []int{55, 66, 77}
fmt.Println("Start slice: ", slice)
fmt.Println("Start slice2:", slice2)

// Добавляем элемент в срез.
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

// Добавляем один срез в другой.
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

// Делаем копию среза (int).
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

// Копируем срез в свой конец.
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)

Вывод:

Start slice:  [1 2 3]
Start slice2: [55 66 77]
Add one item: [1 2 3 4]
Add one slice: [1 2 3 4 55 66 77]
Copy a slice: [1 2 3 4 55 66 77]
Before append to self: [1 2 3 4 55 66 77]
After append to self: [1 2 3 4 55 66 77 1 2 3 4 55 66 77]

Nil

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

sliceHeader{
    Length:        0,
    Capacity:      0,
    ZerothElement: nil,
}

или просто

sliceHeader{}

Ключевым моментом является то, что указатель элемента также равен nil. Срез, созданный

array[0:0]

имеет нулевую длину (и, возможно, даже нулевую емкость), но его указатель не равен nil, поэтому это не нулевой срез.

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

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

Строки

Теперь краткий раздел о строках в Go в контексте срезов.

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

Поскольку они доступны только для чтения, нет необходимости в емкости (вы не можете увеличить их), но в большинстве случаев вы можете обращаться с ними так же, как с байтами только для чтения.

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

slash := "/usr/ken"[0] // возвращает значение байта '/'.

Мы можем нарезать строку, чтобы получить подстроку:

usr := "/usr/ken"[0:4] // возвращает строку "/usr"

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

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

str := string(slice)

или обратное преобразование:

slice := []byte(usr)

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

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

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

Заключение

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

Как только вы оцените, как они работают, срезы становятся не только простыми в использовании, но и мощными и выразительными, особенно с помощью встроенных функций copy и append.


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


пятница, 14 февраля 2020 г.

Тип, значение и равенство интерфейсов в Golang

Тип интерфейса

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

В этом примере и Temp, и *Point реализуют интерфейс MyStringer.

type MyStringer interface {
    String() string
}

type Temp int

func (t Temp) String() string {
    return strconv.Itoa(int(t)) + " °C"
}

type Point struct {
    x, y int
}

func (p *Point) String() string {
    return fmt.Sprintf("(%d,%d)", p.x, p.y)
}

На самом деле, *Temp также реализует MyStringer, так как набор методов типа указателя *T является набором всех методов с приемником *T или T.

Когда вы вызываете метод для значения интерфейса, выполняется метод его базового типа.

var x MyStringer

x = Temp(24)
fmt.Println(x.String()) // 24 °C

x = &Point{1, 2}
fmt.Println(x.String()) // (1,2)

Структурная типизация

Тип реализует интерфейс, реализуя его методы. Никакого явного объявления не требуется.

Фактически, типы Temp, *Temp и *Point также реализуют стандартный интерфейс библиотеки fmt.Stringer. Метод String в этом интерфейсе используется для печати значений, передаваемых в качестве операнда таким функциям, как fmt.Println.

var x MyStringer

x = Temp(24)
fmt.Println(x) // 24 °C

x = &Point{1, 2}
fmt.Println(x) // (1,2)

Пустой интерфейс

Тип интерфейса, который не указывает никаких методов, называется пустым интерфейсом.

interface{}

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

var x interface{}

x = 2.4
fmt.Println(x) // 2.4

x = &Point{1, 2}
fmt.Println(x) // (1,2)

Функция fmt.Println - главный пример. Она принимает любое количество аргументов любого типа.

func Println(a ...interface{}) (n int, err error)

Значения интерфейса

Значение интерфейса состоит из конкретного значения и динамического типа: [Value, Type]

При вызове fmt.Printf вы можете использовать %v для печати конкретного значения и %T для печати динамического типа.

var x MyStringer
fmt.Printf("%v %T\n", x, x) // <nil> <nil>

x = Temp(24)
fmt.Printf("%v %T\n", x, x) // 24 °C main.Temp

x = &Point{1, 2}
fmt.Printf("%v %T\n", x, x) // (1,2) *main.Point

x = (*Point)(nil)
fmt.Printf("%v %T\n", x, x) //  *main.Point

Нулевым значением типа интерфейса является nil, которое представляется как [nil, nil].

Вызов метода на нулевом интерфейсе является ошибкой во время выполнения. Однако довольно часто пишут методы, которые могут обрабатывать значение получателя [nil, Type], где Type не равен nil.

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

Равенство

Два значения интерфейса равны

  • если они имеют одинаковые конкретные значения и идентичные динамические типы,
  • или если оба равны nil.

Значение t интерфейса типа T и значение x неинтерфейсного типа X равны, если

  • конкретное значение t равно х
  • и динамический тип t идентичен X.

var x MyStringer
fmt.Println(x == nil) // true

x = (*Point)(nil)
fmt.Println(x == nil) // false

Во втором операторе print конкретное значение x равно nil, но его динамический тип - *Point, а не nil.


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