понедельник, 30 марта 2020 г.

Блокировка взаимного исключения (мьютекс) в Golang

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

Иногда удобнее синхронизировать доступ к данным с помощью явной блокировки вместо использования каналов. Для этой цели стандартная библиотека Go предлагает блокировку взаимного исключения sync.Mutex.

Чтобы этот тип блокировки был безопасным, очень важно, чтобы все обращения к совместно используемым данным, как чтение, так и запись, выполнялись только тогда, когда goroutine удерживает блокировку (lock). Одной ошибки одной goroutine достаточно, чтобы ввести гонку данных и сломать программу.

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

В следующем примере мы создаем безопасную и простую в использовании конкурентную структуру данных AtomicInt, которая хранит одно целое число. Любое количество goroutine может безопасно получить доступ к этому числу с помощью методов Add и Value.

// AtomicInt - это конкурентная структура данных, 
// которая хранит int.
// Его нулевое значение равно 0.
type AtomicInt struct {
    mu sync.Mutex // Блокировка, которая может удерживаться одной goroutine за раз.
    n  int
}

// Add добавляет n к AtomicInt 
// как отдельная атомарная операция.
func (a *AtomicInt) Add(n int) {
    a.mu.Lock() // Дожидаемся освобождения блокировки (lock), а затем берем ее.
    a.n += n
    a.mu.Unlock() // Снимаем блокировку (lock).
}

// Value возвращает значение a.
func (a *AtomicInt) Value() int {
    a.mu.Lock()
    n := a.n
    a.mu.Unlock()
    return n
}

func main() {
    wait := make(chan struct{})
    var n AtomicInt
    go func() {
        n.Add(1) // один доступ
        close(wait)
    }()
    n.Add(1) // другой конкурентный доступ
    <-wait
    fmt.Println(n.Value()) // 2
}


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


пятница, 27 марта 2020 г.

Таймер и Тикер в Golang

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

Таймаут (Таймер)

time.After ожидает указанную продолжительность и затем отправляет текущее время по возвращаемому каналу:

select {
case news := <-AFP:
    fmt.Println(news)
case <-time.After(time.Hour):
    fmt.Println("No news in an hour.")
}

Подлежащий time.Timer не будет восстановлен сборщиком мусора, пока не сработает таймер. Если это проблема, используйте взамен time.NewTimer и вызовите его метод Stop, когда таймер больше не нужен:

for alive := true; alive; {
    timer := time.NewTimer(time.Hour)
    select {
    case news := <-AFP:
        timer.Stop()
        fmt.Println(news)
    case <-timer.C:
        alive = false
        fmt.Println("No news in an hour. Service aborting.")
    }
}

Повтор (Тикер)

time.Tick возвращает канал, который доставляет такты (ticks) с одинаковыми интервалами:

go func() {
    for now := range time.Tick(time.Minute) {
        fmt.Println(now, statusUpdate())
    }
}()

Подлежащий time.Ticker не будет восстановлен сборщиком мусора. Если это проблема, используйте взамен time.NewTicker и вызовите его метод Stop, когда тикер больше не нужен.

Ожидать, действовать и отменять

time.AfterFunc ждет в течение указанной продолжительности и затем вызывает функцию в своей собственной goroutine. Возвращает time.Timer, который можно использовать для отмены вызова:

func Foo() {
    timer = time.AfterFunc(time.Minute, func() {
        log.Println("Foo выполняется более минуты.")
    })
    defer timer.Stop()

    // Выполняем какую-либо работу
}


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


Купить gopher

четверг, 26 марта 2020 г.

Как остановить goroutine в Golang

Одна goroutine не может принудительно остановить другую.

Чтобы сделать goroutine остановливаемой, позвольте ей слушать сигнал остановки на канале.

quit := make(chan struct{})
go func() {
    for {
        select {
        case <-quit:
            return
        default:
            // …
        }
    }
}()
// …
close(quit)

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

// Generator возвращает канал, 
// который производит числа 1, 2, 3,…
// Чтобы остановить подлежащую goroutine, закройте канал.
func Generator() chan int {
    ch := make(chan int)
    go func() {
        n := 1
        for {
            select {
            case ch <- n:
                n++
            case <-ch:
                return
            }
        }
    }()
    return ch
}

func main() {
    number := Generator()
    fmt.Println(<-number)
    fmt.Println(<-number)
    close(number)
    // …
}

Вывод:

1
2


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


Купить gopher

среда, 25 марта 2020 г.

Передача сигнала по каналу в Golang

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

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

// Печатает текст по истечении заданного времени.
// По завершении канал wait закрывается.
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println("НОВОСТИ:", text)
        close(ch) // Транслируем для всех получателей.
    }()
    return ch
}

Обратите внимание, что мы используем канал пустых структур: struct{}. Это ясно указывает на то, что канал будет использоваться только для сигнализации, а не для передачи данных.

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

func main() {
    wait := Publish("Каналы позволяют goroutines общаться.", 5*time.Second)
    fmt.Println("Ожидание новостей...")
    <-wait
    fmt.Println("Время выходить.")
}

Вывод:

Ожидание новостей...
НОВОСТИ: Каналы позволяют goroutines общаться.
Время выходить.


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


Купить gopher

вторник, 24 марта 2020 г.

Как отлаживать deadlocks в Golang

Deadlock возникает, когда группа goroutines ждет друг друга, и ни одна из них не может продолжить.

Взгляните на этот простой пример.

func main() {
    ch := make(chan int)
    ch <- 1
    fmt.Println(<-ch)
}

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

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    .../deadlock.go:7 +0x6c

Советы по отладке

goroutine может застрять

  • либо потому, что она ждет канал (channel)
  • либо потому, что она ожидает одну из блокировок (lock) в пакете sync.

Общие причины в том, что

  • ни одна другая goroutine не имеет доступа к каналу или блокировке (lock),
  • группа goroutines ждут друг друга, и никто из них не может продолжить.

В настоящее время Go определяет только, когда программа в целом зависает, а не когда застревает подгруппа goroutines.

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


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


Купить gopher

понедельник, 23 марта 2020 г.

Как обнаружить гонки данных в Golang

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

Используйте -race, чтобы включить встроенный детектор гонки данных.

$ go test -race [packages]
$ go run -race [packages]

Пример

Вот программа с гонкой данных:

package main
import "fmt"

func main() {
    i := 0
    go func() {
        i++ // запись
    }()
    fmt.Println(i) // конкурентное чтение
}

Запуск этой программы с опцией -race говорит нам о гонке между записью в строке 7 и чтением в строке 9:

$ go run -race main.go
0
==================
WARNING: DATA RACE
Write by goroutine 6:
  main.main.func1()
      /tmp/main.go:7 +0x44

Previous read by main goroutine:
  main.main()
      /tmp/main.go:9 +0x7e

Goroutine 6 (running) created at:
  main.main()
      /tmp/main.go:8 +0x70
==================
Found 1 data race(s)
exit status 66

Детали

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

Он работает на darwin/amd64, freebsd/amd64, linux/amd64 и windows/amd64.

Накладные расходы варьируются, но обычно увеличивается объем используемой памяти в 5-10 раз, а время выполнения увеличивается в 2-20 раз.


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


Купить gopher

четверг, 19 марта 2020 г.

Гонки данных в Golang простыми словами

Гонка данных (data race) происходит, когда две go-процедуры (goroutines) одновременно обращаются к одной и той же переменной, и, по крайней мере, одно из обращений является записью.

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

Эта функция имеет гонку данных, и ее поведение не определено. Она может, например, напечатать число 1 (хотя задумывалось 2).

func race() {
    wait := make(chan struct{})
    n := 0
    go func() {
        n++ // чтение, приращение, запись
        close(wait)
    }()
    n++ // конфликт доступа
    <-wait
    fmt.Println(n) // Вывод: неопределен
}

Две goroutines, g1 и g2, участвуют в гонке, и нет никакого способа узнать, в каком порядке будут проходить операции. Следующее является одним из многих возможных результатов.

g1 g2
Читает значение 0 из n.
Читает значение 0 из n.
Увеличивает значение с 0 до 1.
Записывает 1 в n.
Увеличивает значение с 0 до 1.
Записывает 1 в n.
Печатает n, которое сейчас равно 1.

Название "гонка данных" (data race) вводит в заблуждение. Не только порядок операций не определен - очень мало гарантий. И компиляторы, и аппаратные средства часто переворачивают код с ног на голову и выворачивают его наизнанку для достижения лучшей производительности. Если вы посмотрите на поток в середине действия, вы можете увидеть почти все что угодно.

Как избежать гонок данных

Единственный способ избежать гонки данных - это синхронизировать доступ ко всем изменяемым данным, которые совместно используются потоками. Есть несколько способов добиться этого. В Go вы обычно используете канал или блокировку (lock). (Низкоуровневые механизмы доступны в пакетах sync и sync/atomic.)

Предпочтительным способом обработки одновременного доступа к данным в Go является использование канала для передачи фактических данных из одной go-процедуры в другую. Девиз: "Не общайтесь, делясь памятью; делитесь памятью, общаясь".

func sharingIsCaring() {
    ch := make(chan int)
    go func() {
        n := 0 // Локальная переменная видна только одной 
               // goroutine.
        n++
        ch <- n // Данные покидают одну goroutine...
    }()
    n := <-ch // ...и благополучно прибывает в другую.
    n++
    fmt.Println(n) // Вывод: 2
}

В этом коде канал выполняет двойную функцию:

  • он передает данные из одной goroutine в другую,
  • и он действует как точка синхронизации.

Отправляющая goroutine будет ждать, пока другая goroutine получит данные, а принимающая goroutine будет ждать, пока другая goroutine отправит данные.

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


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


Купить gopher

вторник, 17 марта 2020 г.

Select ожидает по группе каналов

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

Оператор блокируется целиком, пока одна из операций не станет разблокированной.

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

// блокируется, пока не появятся данные, 
// доступные на ch1 или ch2
select {
case <-ch1:
    fmt.Println("Получено из ch1")
case <-ch2:
    fmt.Println("Получено из ch2")
}

Операции отправки и получения по nil каналу блокируется навсегда. Это можно использовать для отключения канала в операторе select:

ch1 = nil // отключает этот канал
select {
case <-ch1:
    fmt.Println("Получено из ch1") // не произойдет
case <-ch2:
    fmt.Println("Получено из ch2")
}

default случай

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

// никогда не блокирует
select {
case x := <-ch:
    fmt.Println("Получено", x)
default:
    fmt.Println("Нет ничего доступного")
}

Примеры


Бесконечная случайная двоичная последовательность

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

rand := make(chan int)
for {
    select {
    case rand <- 0: // нет оператора
    case rand <- 1:
    }
}

Операция блокировки с тайм-аутом

Функция time.After является частью стандартной библиотеки; она ожидает истечения заданного времени и затем отправляет текущее время по возвращаемому каналу.

select {
case news := <-AFP:
    fmt.Println(news)
case <-time.After(time.Minute):
    fmt.Println("Время вышло: Нет новостей в течение минуты")
}

Утверждение, которое блокирует навсегда

Оператор select блокируется до тех пор, пока не будет продолжен хотя бы один из его случаев. При нулевых случаях это никогда не произойдет.

select {}

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


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


Купить gopher

понедельник, 16 марта 2020 г.

Каналы в Golang предлагают синхронизированное общение

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

Новое значение канала можно задать с помощью встроенной функции make.

// небуферизованный канал целых чисел
ic := make(chan int)

// буферизованный канал с местом для 10 строк
sc := make(chan string, 10)

Чтобы отправить значение в канал, используйте <- в качестве бинарного оператора. Чтобы получить значение из канала, используйте <- его как унарный оператор.

ic <- 3   // Отправляем 3 по каналу
n := <-sc // Получаем string из канала

Оператор <- указывает направление канала, отправлять или получать. Если направление не указано, канал является двунаправленным.

chan Sushi    // может использоваться для отправки 
              // и получения значений типа Sushi

chan<- string // может использоваться только 
              // для отправки строк

<-chan int    // может использоваться только 
              // для получения целых чисел

Буферизованные и небуферизованные каналы

Если пропускная способность канала равна нулю или отсутствует, канал не буферизуется, и отправитель блокируется, пока получатель не получит значение.

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

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

Отправка или получение из нулевого канала блокирует навсегда.

Закрытие канала

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

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

ch := make(chan string)
go func() {
    ch <- "Hello!"
    close(ch)
}()

fmt.Println(<-ch) // Печатает "Hello!".
fmt.Println(<-ch) // Печатает нулевое значение строки - "" 
                  // без блокировки.
fmt.Println(<-ch) // Еще раз печатает "".
v, ok := <-ch     // v равно "", ok равно false.

// Получаем значения от ch до закрытия.
for v := range ch {
    fmt.Println(v) // Не будет выполнено.
}

Пример

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

// Publish печатает текст на стандартный вывод 
// по истечении заданного времени.
// Он закрывает канал wait после публикации текста.
func Publish(text string, delay time.Duration) (wait <-chan struct{}) {
    ch := make(chan struct{})
    go func() {
        time.Sleep(delay)
        fmt.Println(text)
        close(ch)
    }()
    return ch
}

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

wait := Publish("important news", 2 * time.Minute)
// Выполняем еще немного работы.
<-wait // Блокирует, пока текст не будет опубликован.


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


Купить gopher

воскресенье, 15 марта 2020 г.

Goroutines - легкие потоки

Оператор go запускает функцию в отдельном потоке выполнения.

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

go list.Sort() // Запустить list.Sort параллельно; не ждите этого.

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

func main() {
    go fmt.Println("Привет из другой goroutine")
    fmt.Println("Привет из главной goroutine")

    // На этом этапе выполнение программы останавливается
    // и все активные goroutines убиты.
}

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

func main() {
    go fmt.Println("Привет из другой goroutine")
    fmt.Println("Привет из главной goroutine")

    // даем время другой goroutine завершиться
    time.Sleep(time.Second) 
}

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

// Publish выводит текст на стандартный вывод 
// по истечении заданного времени.
// Она не блокируется, но сразу возвращается.
func Publish(text string, delay time.Duration) {
    go func() {
        time.Sleep(delay)
        fmt.Println("BREAKING NEWS:", text)
    }() // Обратите внимание на круглые скобки. 
        // Мы должны вызвать анонимную функцию.
}

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

func main() {
    Publish("goroutine запускается в новом потоке", 5*time.Second)
    fmt.Println("Будем надеяться, что новости будут опубликованы до того как я выйду")

    // Дождаться публикации новостей.
    time.Sleep(10 * time.Second)

    fmt.Println("Десять секунд спустя: я выхожу сейчас")
}

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

$ go run publish1.go
Будем надеяться, что новости будут опубликованы до того как я выйду
BREAKING NEWS: goroutine запускается в новом потоке
Десять секунд спустя: я выхожу сейчас

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

Реализация

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

Внутренние goroutines действуют как сопрограммы (coroutines), которые мультиплексируются между несколькими потоками операционной системы. Если одна goroutine блокирует поток ОС, например, ожидая ввода, другие goroutines в этом потоке будут мигрировать, чтобы продолжить работу.


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


Купить gopher

суббота, 14 марта 2020 г.

Использование fmt.Printf в Golang

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

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

Printf

В этом примере fmt.Printf форматирует и записывает в стандартный вывод:

fmt.Printf("Binary: %b\\%b", 4, 5) 
// Печатает `Binary: 100\101`

  • строка шаблона имеет вид "Binary: %b\\%b"
  • глагол аннотации %b форматирует число в двоичном формате
  • специальное значение \\ является обратной косой чертой.

В особом случае глагол %%, который не использует аргументов, производит знак процента:

fmt.Printf("%d %%", 50) 
// Печатает `50 %`

Sprintf (форматирование без печати)

Используйте fmt.Sprintf для форматирования строки без ее печати:

s := fmt.Sprintf("Binary: %b\\%b", 4, 5) 
// s == `Binary: 100\101`

Нахождение ошибок fmt с помощью vet

Если вы попытаетесь скомпилировать и запустить эту некорректную строку кода

fmt.Printf("Binary: %b\\%b", 4) 
// Отсуствует аргумент для Printf

вы обнаружите, что программа скомпилируется, а затем напечатает

Binary: 100\%!b(MISSING)

Для раннего обнаружения ошибок такого типа вы можете использовать команду vet - она ​​может найти вызовы, аргументы которых не совпадают со строкой формата.

$ go vet example.go
example.go:8: missing argument for Printf("%b"): format reads arg 2, have only 1 args

Варианты использования fmt.Printf


Стандартные форматы и тип

Значение: []int64{0, 1}

Формат Глагол Описание
[0 1] %v Формат по умолчанию
[]int64{0, 1} %#v Формат синтаксиса Go
[]int64 %T Тип значения

Целое число (отступ, основание, знак)

Значение: 15

Формат
Глагол
Описание
15 %d Основание 10
+15 %+d Всегда показывать знак
␣␣15
%4d Отступ с пробелами (ширина 4, выравнивание по правому краю)
15␣␣
%-4d Отступ с пробелами (ширина 4, выравнивание по левому краю)
0015 %04d Отступ с нулями (ширина 4)
1111 %b Основание 2
17 %o Основание 8
f %x Основание 16, нижний регистр
F %X Основание 16, верхний регистр
0xf %#x Основание 16, с ведущим 0x

Символ (цитата, Unicode)

Значение: 65 (Unicode буква A)

Формат Глагол Описание
A %c Символ
'A' %q Цитируемый символ
U+0041 %U Unicode
U+0041 'A' %#U Unicode с символом

Логическое (true/false)

Используйте %t, чтобы отформатировать логическое значение как true или false.

Указатель (шестнадцатеричный)

Используйте %p для форматирования указателя с основанием 16 с начальным 0x.

Float (отступ, точность, научная запись)

Значение: 123.456

Формат Глагол Описание
1.234560e+02
%e Научная запись
123.456000 %f Десятичная точка, без экспоненты
123.46 %.2f Ширина по умолчанию, точность 2
␣␣123.46
%8.2f
Ширина 8, точность 2
123.456 %g Экспонент по мере необходимости, только необходимые цифры

Срез строк или байтов (кавычка, отступ, шестнадцатеричный)

Значение: "café"

Формат Глагол Описание
café %s Обычная строка
␣␣café %6s Ширина 6, выравнивание по правому краю
café␣␣ %-6s Ширина 6, выравнивание по левому краю
"café" %q Цитируемая строка
636166c3a9 %x шестнадцатеричный дамп байтовых значений
63 61 66 c3 a9 % x шестнадцатеричный дамп с пробелами

Специальные значения

Значение Описание
\a U+0007 оповещение или звонок
\b U+0008 backspace
\\ U+005c обратный слэш
\t U+0009 горизонтальный таб
\n U+000A перевод строки (line feed) или новая строка (newline)
\f U+000C form feed
\r U+000D возврат каретки
\v U+000b вертикальный таб

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

Существует четыре разных формата:

  • \x, за которыми следуют ровно две шестнадцатеричные цифры
  • \ за которыми следуют ровно три восьмеричные цифры
  • \u за которыми следуют ровно четыре шестнадцатеричные цифры
  • \U за которыми следуют ровно восемь шестнадцатеричных цифр

Экранирование \u и \U представляет кодовые точки Unicode.

fmt.Println("\\caf\u00e9") // Печатает \café


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


Купить gopher

четверг, 12 марта 2020 г.

Руны и кодировка символов в Golang

Символы, ASCII и Unicode

Тип rune является псевдонимом для int32 и используется, чтобы подчеркнуть, что целое число представляет кодовую точку.

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

Unicode, который является надмножеством ASCII, определяет кодовое пространство из 1114112 кодовых точек. Unicode версия 10.0 охватывает 139 современных и исторических скриптов (включая рунический алфавит, но не клингон), а также множество наборов символов.

Строки и кодировка UTF-8

Строка - это последовательность байтов, а не рун.

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

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

Например, в строке "café" символ é (кодовая точка 233) кодируется с использованием двух байтов, тогда как символы ASCII c, a и f (кодовые точки 99, 97 и 102) используют только один:

fmt.Println([]byte("café")) // [99 97 102 195 169]
fmt.Println([]rune("café")) // [99 97 102 233]


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


Купить gopher

вторник, 10 марта 2020 г.

Обзор обработки строк в Golang

Строковые литералы (экранированные символы)

Выражение Результат Описание
"" Нулевое значение по умолчанию для типа string
"Japan 日本"
Japan 日本
Код Go это Unicode текст, кодированный в UTF‑8
"\xe6\x97\xa5" \xNN определяет byte
"\u65E5" \uNNNN определяет Unicode значение
"\\" \ Обратный слэш
"\"" " Двойная кавычка
"\n" Новая строка
"\t" Символ табуляции
`\xe6` \xe6 Необработанный строковый литерал
html.EscapeString("<>")
&lt;&gt; HTML экранирование для <, >, &, ' и "
url.PathEscape("A B")
A%20B URL %-кодирование net/url

В необработанных строковых литералах (заключены в обратные кавычки ``) текст интерпретируется буквально и обратные слэши не имеют специального значения.

Конкатенация

Выражение Результат Описание
"Ja" + "pan" Japan Конкатенация

Эквивалетность и сравнение (игнорируя регистр)

Выражение
Результат
Описание
"Japan" == "Japan" true Эквивалетность
strings.EqualFold("Japan", "JAPAN") true Сравнение Unicode с оберткой регистра
"Japan" < "japan" true Сравнение в лексикографическом порядке

Длина в байтах или рунах

Выражение
Результат
Описание
len("日") 3 Длина в байтах
utf8.RuneCountInString("日")
1 Длина в рунах unicode/utf8
utf8.ValidString("日") true Проверка UTF-8 ли переданная строка (unicode/utf8)

Индексирование, создание подстроки, итерация

Выражение Результат Описание
"Japan"[2] 'p' Байт в позиции 2
"Japan"[1:3] ap Индексирование байтов
"Japan"[:2] Ja
"Japan"[2:] pan

Цикл range в Go производит итерацию по UTF-8-кодированным символам (руны):

for i, ch := range "Japan 日本" {
    fmt.Printf("%d:%q ", i, ch)
}
// Вывод: 0:'J' 1:'a' 2:'p' 3:'a' 4:'n' 5:' ' 6:'日' 9:'本'

Итерация по байтам производит бессмысленные символы для не-ASCII текста:

s := "Japan 日本"
for i := 0; i < len(s); i++ {
    fmt.Printf("%q ", s[i])
}
// Вывод: 'J' 'a' 'p' 'a' 'n' ' ' 'æ' '\u0097' '¥' 'æ' '\u009c' '¬'

Поиск (contains, prefix/suffix, index)

Выражение
Результат
Описание
strings.Contains("Japan", "abc") false abc в Japan?
strings.ContainsAny("Japan", "abc") true a, b или c в Japan?
strings.Count("Banana", "ana") 1 не перекрывающиеся экземпляры ana
strings.HasPrefix("Japan", "Ja") true Japan начинается с Ja?
strings.HasSuffix("Japan", "pan") true Japan заканчивается на pan?
strings.Index("Japan", "abc") -1 Индекс первого abc
strings.IndexAny("Japan", "abc") 1 Индекс первого a, b или c
strings.LastIndex("Japan", "abc") -1 Индекс последнего abc
strings.LastIndexAny("Japan", "abc") 3 Индекс последнего a, b или c

Замена (uppercase/lowercase, trim)

Выражение
Результат
Описание
strings.Replace("foo", "o", ".", 2) f.. Замена первых двух “o” на “.”. Используйте -1, чтобы заменить все подходящие символы.

f := func(r rune) rune {
    return r + 1
}
strings.Map(f, "ab")

bc Применяет функцию к каждому символу
strings.ToUpper("Japan") JAPAN Приведение к верхнему регистру
strings.ToLower("Japan") japan Приведение к нижнему регистру
strings.Title("ja pan")
Ja Pan
Приведение к верхнему регистру первых букв
strings.TrimSpace(" foo\n") foo Удаление начального и завершающего пробела
strings.Trim("foo", "fo") Удаление начальных и завершающих f и s
strings.TrimLeft("foo", "f") oo Удаление начальных f
strings.TrimRight("foo", "o") f Удаление завершающих f
strings.TrimPrefix("foo", "fo") o Удаление префикса
strings.TrimSuffix("foo", "o") fo Удаление суффикса

Разделение по пробелам или запятым

Выражение Результат Описание
strings.Fields(" a\t b\n") ["a" "b"] Удаление пробелов
strings.Split("a,b", ",") ["a" "b"] Удаление разделителя
strings.SplitAfter("a,b", ",") ["a," "b"] Сохранение разделителя

Объединение строки с разделителем

Выражение Результат Описание
strings.Join([]string{"a", "b"}, ":") a:b Добавление разделителя
strings.Repeat("da", 2) dada 2 копии “da”

Форматирование и конвертирование

Выражение
Результат
Описание
strconv.Itoa(-42) "-42" Int к string
strconv.FormatInt(255, 16) "ff" Конвертирование к шестнадцетиричной основе

Sprintf

Функция fmt.Sprintf часто лучший выбор для форматирования данных:

s := fmt.Sprintf("%.4f", math.Pi) // s == "3.1416"


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


Купить gopher

пятница, 6 марта 2020 г.

Эффективная конкатенация строк в Golang

Простое создание строк

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

s := fmt.Sprintf("Size: %d MB.", 85) // s == "Size: 85 MB."

Высокопроизводительная конкатенация строк с версии Go 1.10

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

  • Он предлагает подмножество методов bytes.Buffer, что позволяет безопасно избежать дополнительного копирования при преобразовании Builder в строку.
  • Вы можете использовать пакет fmt для форматирования, поскольку Builder реализует интерфейс io.Writer.
  • Метод Grow может использоваться для предварительного выделения памяти, когда известен максимальный размер строки.

var b strings.Builder
b.Grow(32)
for i, p := range []int{2, 3, 5, 7, 11, 13} {
    fmt.Fprintf(&b, "%d:%d, ", i+1, p)
}
s := b.String()   // нет копирования

s = s[:b.Len()-2] // нет копирования 
                  // (удаляет завершающий ", ")
fmt.Println(s)

Вывод:

1:2, 2:3, 3:5, 4:7, 5:11, 6:13

Создание строк до версии Go 1.10

Используйте fmt.Fprintf для печати в bytes.Buffer.

var buf bytes.Buffer
for i, p := range []int{2, 3, 5, 7, 11, 13} {
    fmt.Fprintf(&buf, "%d:%d, ", i+1, p)
}
buf.Truncate(buf.Len() - 2) // Удаляет завершающий ", "
s := buf.String()           // Копирует в новую строку
fmt.Println(s)

Вывод:

1:2, 2:3, 3:5, 4:7, 5:11, 6:13

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

buf := []byte("Size: ")
buf = strconv.AppendInt(buf, 85, 10)
buf = append(buf, " MB."...)
s := string(buf)

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

buf := make([]byte, 0, 16)
buf = append(buf, "Size: "...)
buf = strconv.AppendInt(buf, 85, 10)
buf = append(buf, " MB."...)
s := string(buf)


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


Купить gopher

среда, 4 марта 2020 г.

Regexp (регулярные выражения) в Golang

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

Регулярное выражение a.b соответствует любой строке, которая начинается с a, заканчивается на b и содержит один символ между ними (точка соответствует любому символу).

Чтобы проверить, существует ли подстрока, соответствующая a.b, используйте функцию regexp.MatchString.

matched, err := regexp.MatchString(`a.b`, "aaxbb")
fmt.Println(matched) // true
fmt.Println(err)     // nil (regexp валидно)

Чтобы проверить, соответствует ли полная строка a.b, закрепите начало и конец регулярного выражения:

  • символ каретки ^ соответствует началу текста или строки,
  • символ доллара $ соответствует концу текста.

matched, _ := regexp.MatchString(`^a.b$`, "aaxbb")
fmt.Println(matched) // false

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

Compile

Для более сложных запросов вы должны скомпилировать регулярное выражение для создания объекта Regexp. Есть два варианта:

re1, err := regexp.Compile(`regexp`) // error если regexp невалидно
re2 := regexp.MustCompile(`regexp`)  // panic если regexp невалидно

Необработанные строки (Raw strings)

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

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

Регулярные выражения


Выбор и группировка


Regexp Значение
xy х с последующим у
x|y х или у, предпочитает х
xy|z такой же, как (xy)|z
xy* такой же как х(у*)

Повторение (жадное (greedy) и не жадное (non-greedy))


Regexp Значение
x* ноль или более х, предпочитает больше
x*? ноль или более х, предпочитает меньше (не жадное)
x+ один или несколько х, предпочитает больше
x+? один или несколько х, предпочитает меньше (не жадное)
x? ноль или один х, предпочитает один
x?? ноль или один х, предпочитает ноль
x{n} x точно n раз

Классы символов


Regexp Значение
. (точка) любой символ
[ab] символ a или b
[^ab] любой символ исключая a или b
[a-z] любой символ от a до z
[a-z0-9] любой символ от a до z или от 0 до 9
\d цифра: [0-9]
\D не цифра: [^0-9]
\s пробельный символ: [\t\n\f\r ]
\S не пробельный символ: [^\t\n\f\r ]
\w символ слова: [0-9A-Za-z_]
\W несловесный символ: [^0-9A-Za-z_]
\p{Greek} класс символов Unicode (RE2: имена классов символов Unicode)
\pN однобуквенное имя
\P{Greek} исключая класс символов Unicode (RE2: имена классов символов Unicode)
\PN исключая класс символов Unicode - однобуквенное имя

Специальные символы

Чтобы сопоставить специальный символ \^$.|?*+-[]{}() буквально, экранируйте (escape) его обратной косой чертой. Например, \{ соответствует открывающей фигурной скобке.

Другие escape-последовательности:


Символ Значение
\t символ горизонтальной табуляции (horizontal tab) = \011
\n символ новой строки (newline) = \012
\f form feed = \014
\r символ возврата каретки = \015
\v символ вертикальной табуляции (vertical tab) = \013
\123 восьмеричный код символа (до трех цифр)
\x7F шестнадцатеричный код (ровно две цифры)

Якори границ текста


Символ Значение
\A в начале текста
^ в начале текста или строки
$
\z
в конце текста
\b на границе ASCII слова
\B не на границе ASCII слова

Регистрозависимые и многострочные совпадения

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

Например, префикс "(?is)" делает сопоставление без учета регистра и позволяет . (точке) совпадать \n. (Соответствие по умолчанию чувствительно к регистру и . (точка) не соответствует \n.)

Флаг Значение
i не чувствительны к регистру
m позволяет ^ и $ соотвествовать началу/концу строки в дополнение к началу/концу текста (многострочный режим)
s позволяет . (точка) соотвествовать \n (однострочный режим)

Примеры кода


Первое совпадение

Используйте метод FindString, чтобы найти текст первого совпадения. Если совпадений нет, возвращаемое значение - пустая строка.

re := regexp.MustCompile(`foo.?`)
fmt.Printf("%q\n", re.FindString("seafood fool")) // "food"
fmt.Printf("%q\n", re.FindString("meat"))         // ""

Место расположения

Используйте метод FindStringIndex, чтобы найти loc, местоположение первого совпадения, в строке s. Совпадение в s[loc[0]:loc[1]]. Возвращаемое значение nil указывает на отсутствие совпадения.

re := regexp.MustCompile(`ab?`)
fmt.Println(re.FindStringIndex("tablett"))    // [1 3]
fmt.Println(re.FindStringIndex("foo") == nil) // true

Все совпадения

Используйте метод FindAllString, чтобы найти текст всех совпадений. Возвращаемое значение nil указывает на отсутствие совпадения.

Метод принимает целочисленный аргумент n; если n >= 0, функция возвращает не более n совпадений.

re := regexp.MustCompile(`a.`)
fmt.Printf("%q\n", re.FindAllString("paranormal", -1)) // ["ar" "an" "al"]
fmt.Printf("%q\n", re.FindAllString("paranormal", 2))  // ["ar" "an"]
fmt.Printf("%q\n", re.FindAllString("graal", -1))      // ["aa"]
fmt.Printf("%q\n", re.FindAllString("none", -1))       // [] (nil slice)

Замещение (Replace)

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

re := regexp.MustCompile(`ab*`)
fmt.Printf("%q\n", re.ReplaceAllString("-a-abb-", "T")) // "-T-T-"

Метод Split

Используйте метод Split, чтобы разрезать строку на подстроки, разделенные регулярным выражением. Он возвращает часть подстрок между этими совпадениями выражений. Возвращаемое значение nil указывает на отсутствие совпадения.

Метод принимает целочисленный аргумент n; если n >= 0, функция возвращает не более n совпадений.

a := regexp.MustCompile(`a`)
fmt.Printf("%q\n", a.Split("banana", -1)) // ["b" "n" "n" ""]
fmt.Printf("%q\n", a.Split("banana", 0))  // [] (nil slice)
fmt.Printf("%q\n", a.Split("banana", 1))  // ["banana"]
fmt.Printf("%q\n", a.Split("banana", 2))  // ["b" "nana"]

zp := regexp.MustCompile(`z+`)
fmt.Printf("%q\n", zp.Split("pizza", -1)) // ["pi" "a"]
fmt.Printf("%q\n", zp.Split("pizza", 0))  // [] (nil slice)
fmt.Printf("%q\n", zp.Split("pizza", 1))  // ["pizza"]
fmt.Printf("%q\n", zp.Split("pizza", 2))  // ["pi" "a"]

Больше функций

Есть 16 функций, следующих шаблону именования

Find(All)?(String)?(Submatch)?(Index)?

Например: Find, FindAllString, FindStringIndex, ...

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

Реализация

  • Пакет regexp реализует регулярные выражения с синтаксисом RE2.
  • Он поддерживает строки в кодировке UTF-8 и классы символов Unicode.
  • Реализация очень эффективна: время выполнения линейно по размеру ввода.
  • Обратные ссылки не поддерживаются, поскольку они не могут быть эффективно реализованы.

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


Купить gopher