Golang: пакет bytes изнутри
- пятница, 12 июля 2024 г. в 00:00:07
Приветствую, в прошлой статье мы разбирали определение bytes.Buffer
изнутри. Теперь хочется обратить внимание на сам пакет bytes
. Что за ним скрывается? Каждому разработчику приходилось использовать его будь то в production или локальной разработке. Это достаточно мощный по своим меркам пакет, который предоставляет нам функции для работы с байтами.
Давайте разберем каждую функцию отдельно и поговорим какая и зачем нужна, а самое главное посмотрим на исходный код вблизи. Статья может являться неким справочником или просто служить для повторения внутреннего строения пакета. Она достаточно длинная, поэтому не каждому подойдет читать все подряд. Я постарался выделить основные моменты каждой функции и привести примеры, для наглядного понимания принципа работы, так как пакет достаточно широкий и использоваться может во многих задачах. В особенности полезно будет понимание работы для улучшения производительности неких частей ваших сервисов. Приятного чтения!
Начнем с функции, которая определена самой первой с пакете. Посмотрим на ее исходный код:
func Equal(a, b []byte) bool {
return string(a) == string(b)
}
Подметим, что эти преобразования не аллоцируются, как подмечено самими разработчиками. Рассмотрим зачем эта функция нужна.
Equal
сообщает, имеют ли a
и b
одинаковую длину и содержат ли они одинаковые байты.
Аргумент nil
эквивалентен пустому срезу.
Этот подход работает, потому что в Go строки являются неизменяемыми срезами байтов, и их можно сравнивать с помощью оператора ==
. Если оба среза имеют одинаковую длину и содержат одинаковые байты, то преобразованные строки также будут равны.
Пример:
func TryEqual() {
a := []byte("hello")
b := []byte("hello")
c := []byte("world")
fmt.Println(bytes.Equal(a, b)) // true
fmt.Println(bytes.Equal(a, c)) // false
}
Достаточно эффективно использовать эту функцию для сравнения срезов байт, что довольно часто может понадобиться.
Compare
принимает два аргумента типа []byte
(срезы байтов) a
и b
, и возвращает целое число, которое сравнивает эти срезы в лексикографическом порядке. А сам лексикографический порядок - это порядок в неком алфавите.
func TryCompare() {
a := []byte("hello")
b := []byte("hello")
c := []byte("world")
fmt.Println(bytes.Compare(a, b)) // 0
fmt.Println(bytes.Compare(a, c)) // -1
fmt.Println(bytes.Compare(c, a)) // 1
}
Далее мы рассмотрим более глубокую функцию. Count
- это функция из пакета bytes
стандартной библиотеки Go, которая принимает два аргумента типа []byte
(срезы байтов) s
и sep
, и возвращает количество непересекающихся вхождений sep
в s
.
func Count(s, sep []byte) int {
// если sep является пустым срезом,
// вернет количество UTF-8-кодированных точек в s плюс один
if len(sep) == 0 {
return utf8.RuneCount(s) + 1
}
// если sep состоит из одного байта, использует оптимизированную функцию bytealg.Count
if len(sep) == 1 {
return bytealg.Count(s, sep[0])
}
// инициализация счетчика вхождений sep в s нулем
n := 0
// цикл поиска вхождений sep в s
for {
// поиск индекса следующего вхождения sep в s
i := Index(s, sep)
// если вхождений sep не найдено, вернуть счетчик
if i == -1 {
return n
}
// увеличение счетчика вхождений sep в s
n++
// обновление s до подсреза, начинающегося после конца текущего вхождения sep
s = s[i+len(sep):]
}
}
Уже что-то поинтереснее, но давайте взглянем на определение Index
:
// Index возвращает индекс первого вхождения sep в s или -1, если sep не найден в s.
func Index(s, sep []byte) int {
// определение длины sep
n := len(sep)
switch {
case n == 0:
// если sep пустой, возращаем 0
return 0
case n == 1:
// если sep состоит из одного байта, используем оптимизированную функцию IndexByte
return IndexByte(s, sep[0])
case n == len(s):
// если sep имеет ту же длину, что и s, проверяем, равны ли они с помощью функции Equal,
// которую мы разбирали выше
if Equal(sep, s) {
return 0
}
// если не равны, возращаем -1
return -1
case n > len(s):
// если sep длиннее, чем s, возращаем -1
return -1
case n <= bytealg.MaxLen:
// если sep достаточно мал, используем брутфорс для поиска вхождения
// с помощью функции bytealg.Index
// если s также достаточно мал, используем bytealg.Index
if len(s) <= bytealg.MaxBruteForce {
return bytealg.Index(s, sep)
}
c0 := sep[0]
c1 := sep[1]
i := 0
t := len(s) - n + 1
fails := 0
for i < t {
// поиск первого байта sep в s
if s[i] != c0 {
// если байт не найден, используем IndexByte для поиска следующего вхождения
o := IndexByte(s[i+1:t], c0)
if o < 0 {
return -1
}
i += o + 1
}
// проверка второго байта sep и последовательности байтов sep в s
if s[i+1] == c1 && Equal(s[i:i+n], sep) {
return i
}
i++
fails++
// если IndexByte производит слишком много ложных положительных результатов,
// переходим к использованию bytealg.Index
if fails > bytealg.Cutover(i) {
r := bytealg.Index(s[i:], sep)
if r >= 0 {
return r + i
}
return -1
}
}
// если sep не найден, возращаем -1
return -1
}
// если sep не является каким-либо случаем, используем оптимизированный алгоритм поиска
// основанный на алгоритме Рэбина-Карпа
c0 := sep[0]
c1 := sep[1]
i := 0
fails := 0
t := len(s) - n + 1
for i < t {
// поиск первого байта sep в s
if s[i] != c0 {
// если байт не найден, используем IndexByte для поиска следующего вхождения
o := IndexByte(s[i+1:t], c0)
if o < 0 {
break
}
i += o + 1
}
// проверка второго байта sep и последовательности байтов sep в s
if s[i+1] == c1 && Equal(s[i:i+n], sep) {
return i
}
i++
fails++
// если IndexByte производит слишком много ложных результатов,
// переходим к использованию bytealg.IndexRabinKarp
if fails >= 4+i>>4 && i < t {
j := bytealg.IndexRabinKarp(s[i:], sep)
if j < 0 {
return -1
}
return i + j
}
}
// если sep не найден, возращаем -1
return -1
}
Что же за такой алгоритм Рэбина-Карпа? Этот алгоритм ищет подстроку в тексте, используя хеширование. Алгоритм использует хеш-функцию, которая позволяет быстро вычислять хеш-значения для сегментов строки. Это делает алгоритм эффективным для поиска подстрок в больших строках. Больше об этом алгоритме вы сможете прочитать тут. Ну а IndexByte
это просто обертка:
func IndexByte(b []byte, c byte) int {
return bytealg.IndexByte(b, c)
}
IndexByte
возвращает индекс первого экземпляра c
в b
или -1, если c отсутствует в b
.
Пример использования Count
:
func TryCount() {
// создаем срез байтов
s := []byte("hello world hello")
sep := []byte(" ")
fmt.Println(bytes.Count(s, sep)) // 2
}
func Contains(b, subslice []byte) bool {
return Index(b, subslice) != -1
}
Если Index()
возвращает -1
, это означает, что subslice
не найден в b
, и функция Contains()
возвращает false
. Если Index()
возвращает индекс вхождения, это означает, что subslice
найден в b
, и функция Contains()
возвращает true
. Просто некая обертка, как и обсуждалось выше.
Пример:
func TryContains() {
b := []byte("hello world")
subslice := []byte("world")
fmt.Println(bytes.Contains(b, subslice)) // true
subslice2 := []byte("foo")
fmt.Println(bytes.Contains(b, subslice2)) // false
}
func ContainsAny(b []byte, chars string) bool {
return IndexAny(b, chars) >= 0
}
Содержит все сообщения о том, находятся ли какие-либо кодовые точки в кодировке UTF-8 в символах в пределах b
. Эта функция может быть полезна для проверки, содержит ли большой срез байтов любые из заданных символов или кодовых точек, без необходимости создавать новый срез или копировать данные.
Но посмотрим на определение IndexAny
:
func IndexAny(s []byte, chars string) int {
if chars == "" {
// если chars пустая строка, возращаем -1
return -1
}
if len(s) == 1 {
// если s состоит из одного байта, используем оптимизированную функцию IndexByteString для поиска вхождения
r := rune(s[0])
if r >= utf8.RuneSelf {
// поиск utf8.RuneError
for _, r = range chars {
if r == utf8.RuneError {
return 0
}
}
return -1
}
if bytealg.IndexByteString(chars, s[0]) >= 0 {
return 0
}
return -1
}
if len(chars) == 1 {
// если chars состоит из одного символа, используем функцию IndexRune для поиска вхождения
r := rune(chars[0])
if r >= utf8.RuneSelf {
r = utf8.RuneError
}
return IndexRune(s, r)
}
if len(s) > 8 {
// если s достаточно большой, проверяем, является ли chars ASCII-строкой
if as, isASCII := makeASCIISet(chars); isASCII {
// если chars является ASCII-строкой, используем оптимизированную функцию makeASCIISet для создания битового массива
// представляющего множество ASCII-символов в chars
for i, c := range s {
if as.contains(c) {
return i
}
}
return -1
}
}
// используем цикл для поиска первого вхождения любой из кодовых точек в s
var width int
for i := 0; i < len(s); i += width {
r := rune(s[i])
if r < utf8.RuneSelf {
// если r является ASCII-символом, используем оптимизированную функцию IndexByteString для поиска вхождения
if bytealg.IndexByteString(chars, s[i]) >= 0 {
return i
}
width = 1
continue
}
// декодируем r из s с помощью функции utf8.DecodeRune
r, width = utf8.DecodeRune(s[i:])
if r != utf8.RuneError {
// проверяем, является ли r одной из кодовых точек в chars
if len(chars) == width {
if chars == string(r) {
return i
}
continue
}
// используем оптимизированную функцию bytealg.IndexString для поиска вхождения, если это возможно
if bytealg.MaxLen >= width {
if bytealg.IndexString(chars, string(r)) >= 0 {
return i
}
continue
}
}
// проверяем, является ли r одной из кодовых точек в chars
for _, ch := range chars {
if r == ch {
return i
}
}
}
// если кодовые точки не найдены в s, возращаем -1
return -1
}
Ищет первое вхождение любой из UTF-8-кодированных кодовых точек, указанных в строке chars
, в срезе байтов s
, и возвращает индекс первого байта вхождения или -1
, если кодовые точки не найдены в s
.
Пример ContainsAny
:
func TryContainsAny() {
b := []byte("hello world")
chars := "aeiou"
fmt.Println(bytes.ContainsAny(b, chars)) // true
chars2 := "xyz"
fmt.Println(bytes.ContainsAny(b, chars2)) // false
}
Функция ContainsAny
проверяет, содержит ли срез байтов b
любые из UTF-8-кодированных кодовых точек, указанных в строке chars
.
Основное отличие между этими функциями заключается в том, что Contains
ищет точную последовательность байтов, в то время как ContainsAny
ищет любые из указанных кодовых точек. Это означает, что Contains
может быть более эффективной, когда необходимо найти точную подстроку, но менее гибкой, чем ContainsAny
, которая может быть использована для поиска любых из заданных символов или кодовых точек.
Например, функция Contains
может быть использована для проверки, содержит ли срез байтов определенную подстроку, такую как []byte("hello")
, в то время как функция ContainsAny
может быть использована для проверки, содержит ли срез байтов любые из заданных символов, таких как "aeiou"
.
Я думаю, что можно ее пропустить, так как она ведет себя также, как и примеры выше, но вот IndexRune уже поинтереснее.
func ContainsRune(b []byte, r rune) bool {
return IndexRune(b, r) >= 0
}
func IndexRune(s []byte, r rune) int {
switch {
case 0 <= r && r < utf8.RuneSelf:
// если r является ASCII-символом, используем IndexByte для поиска вхождения
return IndexByte(s, byte(r))
case r == utf8.RuneError:
// если r равно utf8.RuneError, ищем первое вхождение любой некорректной последовательности байтов UTF-8
for i := 0; i < len(s); {
r1, n := utf8.DecodeRune(s[i:])
if r1 == utf8.RuneError {
return i
}
i += n
}
return -1
case !utf8.ValidRune(r):
// если r не является допустимой кодовой точкой UTF-8, возращаем -1
return -1
default:
// преобразовываем r в последовательность байтов b и используем Index для поиска вхождения
var b [utf8.UTFMax]byte
n := utf8.EncodeRune(b[:], r)
return Index(s, b[:n])
}
}
В UTF-8 каждая кодовая точка может быть представлена от 1 до 4 байтов. Количество байтов, необходимое для представления кодовой точки, зависит от ее значения. Кодовые точки с низкими значениями, такие как ASCII-символы, представлены одним байтом, в то время как кодовые точки с более высокими значениями, такие как эмодзи, представлены несколькими байтами.
Константа utf8.Max
равна 4, так как максимальное количество байтов, необходимое для представления одной кодовой точки в UTF-8, равно 4. Эта константа может быть использована для определения максимального размера буфера, необходимого для представления строки в UTF-8, или для проверки, что срез байтов представляет допустимую последовательность байтов UTF-8. Более подробно про этот пакет мы поговорим в следующих статьях.
func ContainsFunc(b []byte, f func(rune) bool) bool {
return IndexFunc(b, f) >= 0
}
В целом, ничего необычного и непонятного нет, посмотрим на IndexFunc
:
func LastIndexFunc(s []byte, f func(r rune) bool) int {
return lastIndexFunc(s, f, true)
}
func indexFunc(s []byte, f func(r rune) bool, truth bool) int {
// начальный индекс поиска
start := 0
// цикл для итерации по срезу байтов s
for start < len(s) {
// ширина кодовой точки
wid := 1
// декодирование кодовой точки r из s[start]
r := rune(s[start])
if r >= utf8.RuneSelf {
// если r является первым байтом многобайтовой кодовой точки,
// декодируем полную кодовую точку с помощью функции utf8.DecodeRune()
r, wid = utf8.DecodeRune(s[start:])
}
// проверка, удовлетворяет ли кодовая точка r f(rune) bool
if f(r) == truth {
// если кодовая точка удовлетворяет, вернуть индекс первого байта этой кодовой точки
return start
}
// переход к следующей кодовой точке
start += wid
}
// если кодовая точка, удовлетворяющая функции, не найдена, вернуть -1
return -1
}
Пример использования ContainsFunc
:
func TryContainsFunc() {
b := []byte("hello world")
f := func(r rune) bool {
return unicode.IsUpper(r)
}
fmt.Println(bytes.ContainsFunc(b, f)) // false
b2 := []byte("Hello World")
fmt.Println(bytes.ContainsFunc(b2, f)) // true
}
func LastIndex(s, sep []byte) int {
n := len(sep)
switch {
case n == 0:
return len(s)
case n == 1:
return bytealg.LastIndexByte(s, sep[0])
case n == len(s):
if Equal(s, sep) {
return 0
}
return -1
case n > len(s):
return -1
}
return bytealg.LastIndexRabinKarp(s, sep)
}
И как мы видим, тут тоже используется РэбинКарп. Ищет последнее вхождение и возращает последний индекс вхождения. Пример:
func TryLastIndex() {
s := []byte("hello world")
sep := []byte("o")
fmt.Println(bytes.LastIndex(s, sep)) // 7
sep2 := []byte("foo")
fmt.Println(bytes.LastIndex(s, sep2)) // -1
}
В этом примере мы ищем последнее вхождение буквы "o" в срез байтов s
и последнее вхождение строки "foo" в s
. Функция LastIndex()
возвращает 7
для первого случая и -1
для второго, так как строка "foo" не найдена в s
. На практике мы можем использовать данную функцию для какого-то анализа файлов для поиска последнего вхождения, что может быть полезно.
func LastIndexByte(s []byte, c byte) int {
return bytealg.LastIndexByte(s, c)
}
Практически аналогично, но возращает вхождение байта, а не среза.
Разбивает срез байтов s
на подсрезы, разделенные разделителем sep
, и возвращает срез этих подсрезов.
Функция принимает следующие аргументы:
s
- срез байтов для разбиения.
sep
- разделитель, по которому будет происходить разбиение.
n
- максимальное количество подсрезов, которые должны быть возвращены. Если n
меньше или равно 0, функция возвращает все подсрезы. Если n
больше 0, функция возвращает не более n
подсрезов, и последний подсрез будет содержать неразделенную оставшуюся часть s
.
func SplitN(s, sep []byte, n int) [][]byte { return genSplit(s, sep, 0, n) }
genSplit()
- реализует основной алгоритм разбиения и принимает дополнительный аргумент limit
, который определяет максимальное количество байтов, которые могут быть возвращены в результате. Если limit
равен 0, функция возвращает все подсрезы без ограничений.
Если sep
пустой срез, SplitN
разбивает s
после каждой UTF-8-кодированной кодовой точки.
Пример:
func TrySplitN() {
s := []byte("hello,world,golang,bytes")
sep := []byte(",")
n := 3
subslices := bytes.SplitN(s, sep, n)
for _, subslice := range subslices {
fmt.Println(string(subslice))
}
}
Вывод:
hello
world
golang,bytes
Эта функция может быть полезна для разбиения больших двоичных данных на более мелкие части, разделенные определенным разделителем, например, для парсинга текстовых файлов или обработки пакетов данных в сети.
Посмотрим поближе на genSplit
.
func genSplit(s, sep []byte, sepSave, n int) [][]byte {
// если n равно 0, возвращаем nil
if n == 0 {
return nil
}
// если sep пустой, вызываем вспомогательную функцию explode(), которая разбивает s после каждой UTF-8-кодированной кодовой точки
if len(sep) == 0 {
return explode(s, n)
}
// если n меньше 0, вычисляем количество подсрезов, которые должны быть возвращены, на основе количества разделителей в s
if n < 0 {
n = Count(s, sep) + 1
}
// если n больше длины s, устанавливаем n равным длине s плюс один
if n > len(s)+1 {
n = len(s) + 1
}
// создаем срез a для хранения подсрезов и переменную i для отслеживания текущего индекса подсреза
a := make([][]byte, n)
n--
i := 0
// цикл ищет индекс первого вхождения разделителя sep в s с помощью функции Index()
// если разделитель найден, добавляем подсрез s до индекса разделителя в срез a и обновляем s, чтобы он начинался после разделителя
// цикл продолжается, пока не будет найдено n подсрезов или разделитель не будет найден
for i < n {
m := Index(s, sep)
if m < 0 {
break
}
a[i] = s[: m+sepSave : m+sepSave]
s = s[m+len(sep):]
i++
}
// добавляем оставшуюся часть s в срез a и возвращаем срез a до текущего индекса подсреза плюс один
a[i] = s
return a[:i+1]
}
func explode(s []byte, n int) [][]byte {
// если n меньше или равно 0 или больше длины s, устанавливаем n равным длине s
if n <= 0 || n > len(s) {
n = len(s)
}
// создаем срез a для хранения подсрезов и переменные size и na для отслеживания текущего размера и индекса подсреза
a := make([][]byte, n)
var size int
na := 0
// цикл декодирует первую UTF-8-кодированную кодовую точку из s с помощью функции utf8.DecodeRune()
// если количество подсрезов достигло максимума, добавляем оставшуюся часть s в последний подсрез и прерываем цикл
// в противном случае добавляем подсрез s до текущей кодовой точки в срез a и обновляем s, чтобы он начинался после кодовой точки
for len(s) > 0 {
if na+1 >= n {
a[na] = s
na++
break
}
_, size = utf8.DecodeRune(s)
a[na] = s[0:size:size]
s = s[size:]
na++
}
// возвращаем срез a до текущего индекса подсреза
return a[0:na]
}
В итоге достаточно полезная функция для разбиения среза байт.
Отличие между этими функциями заключается в том, где происходит разбиение относительно разделителя. Функция SplitN
разбивает срез байтов перед разделителем, а функция SplitAfterN
разбивает срез байтов после разделителя.
Например, если у нас есть срез байтов s := []byte("a,b,c")
и разделитель sep := []byte(",")
, то вызов bytes.SplitN(s, sep, 2)
вернет срез [][]byte{[]byte("a"), []byte("b,c")}
, а вызов bytes.SplitAfterN(s, sep, 2)
вернет срез [][]byte{[]byte("a,"), []byte("b,c")}
.
func SplitAfterN(s, sep []byte, n int) [][]byte {
return genSplit(s, sep, len(sep), n)
}
Пример:
func TrySplitAfterN() {
s := []byte("hello,world,golang,bytes")
sep := []byte(",")
n := 3
subslices := bytes.SplitAfterN(s, sep, n)
for _, subslice := range subslices {
fmt.Println(string(subslice))
}
}
Вывод:
hello,
world,
golang,bytes
func Split(s, sep []byte) [][]byte { return genSplit(s, sep, 0, -1) }
Разбивает срез байтов s
на подсрезы, разделенные разделителем sep
, и возвращает срез этих подсрезов. Разбиение происходит перед каждым вхождением разделителя sep
.
Пример:
func TrySplit() {
s := "hello world from Go program"
sep := " "
words := bytes.Split([]byte(s), []byte(sep))
for _, word := range words {
fmt.Println(string(word))
}
}
Вывод:
hello
world
from
Go
program
Функция разбивает срез байтов s
на подсрезы, разделенные одним или более пробельными символами, определенными функцией unicode.IsSpace
. Она возвращает срез подсрезов или пустой срез, если s
содержит только пробельные символы.
func Fields(s []byte) [][]byte {
// сначала подсчитываем количество подсрезов, разделенных пробельными символами
// это точное количество, если s содержит только ASCII-символы, в противном случае испольуется приближение
n := 0
wasSpace := 1
// setBits используется для отслеживания установленных битов в байтах s
setBits := uint8(0)
for i := 0; i < len(s); i++ {
r := s[i]
// побитовое OR между переменной setBits и байтом r, позволяет отслеживать, какие биты были установлены в любом из байтов среза s
setBits |= r
// определение по массиву
isSpace := int(asciiSpace[r])
// увеличивает счетчик n на 1, если предыдущий символ был пробельным, а текущий нет, выполняется с помощью побитовых операций AND и XOR
n += wasSpace & ^isSpace
wasSpace = isSpace
}
// если в срезе есть не-ASCII символы, используем более медленный путь
if setBits >= utf8.RuneSelf {
return FieldsFunc(s, unicode.IsSpace)
}
// создаем срез подсрезов длиной n
a := make([][]byte, n)
// текущий индекс подсреза
na := 0
// индекс начала текущего поля
fieldStart := 0
i := 0
// Пропускаем пробельные символы в начале среза
for i < len(s) && asciiSpace[s[i]] != 0 {
i++
}
fieldStart = i
// проходим по срезу и разбиваем его на подсрезы, разделенные одним или более пробельными символами
for i < len(s) {
if asciiSpace[s[i]] == 0 {
i++
continue
}
// добавляем текущее поле в срез подсрезов
a[na] = s[fieldStart:i:i]
na++
i++
// пропускаем пробельные символы между полями
for i < len(s) && asciiSpace[s[i]] != 0 {
i++
}
fieldStart = i
}
// добавляем последнее поле, если оно не пустое
if fieldStart < len(s) {
a[na] = s[fieldStart:len(s):len(s)]
}
return a
}
Эта функция может быть полезна для разбиения текстовых данных на отдельные слова или токены, разделенные пробельными символами. Например, если у нас есть строка "hello world"
, вызов bytes.Fields([]byte("hello world"))
вернет срез [][]byte{[]byte("hello"), []byte("world")}
. Она использует два пути для разбиения среза байтов на подсрезы: более медленный путь для срезов, содержащих не-ASCII символы, и более быстрый путь для срезов, содержащих только ASCII-символы. Функция сначала подсчитывает количество подсрезов, разделенных пробельными символами, используя переменную n
. Затем она проверяет, содержит ли срез не-ASCII символы, используя переменную setBits
, и выбирает соответствующий путь.
Посмотрим, что же такое asciiSpace
.
var asciiSpace = [256]uint8{'\t': 1, '\n': 1, '\v': 1, '\f': 1, '\r': 1, ' ': 1}
Массив содержит 256 элементов типа uint8
, каждый из которых соответствует одному байту ASCII-таблицы. Значениями элементов массива являются нули и единицы. Если элемент равен нулю, это означает, что соответствующий символ не является пробельным, а если равен единице, то является. Это позволяет избежать вызова функции unicode.IsSpace
для каждого символа, что может быть медленно выполнимым.
Можем заметить, что в коде используются побитовые оптимизации для увеличения производительности.
Такое же назначение, но только по заданному условию.
func FieldsFunc(s []byte, f func(rune) bool) [][]byte {
// определяем структуру span для хранения начального и конечного индексов подсреза
type span struct {
start int
end int
}
// создаем срез для хранения индексов подсрезов
spans := make([]span, 0, 32)
// находим индексы начала и конца подсрезов.
// это делается в отдельном проходе (а не путем разделения среза s и сбора
// результирующих подсрезов сразу), что эффективнее
start := -1 // индекс начала текущего подсреза, если >= 0
for i := 0; i < len(s); {
// определяем размер и значение текущей кодовой точки
size := 1
r := rune(s[i])
if r >= utf8.RuneSelf {
r, size = utf8.DecodeRune(s[i:])
}
// проверяем, удовлетворяет ли кодовая точка условию f(rune) bool
if f(r) {
// если текущий подсрез не пустой, сохраняем его индексы в срез spans
// и сбрасываем индекс начала текущего подсреза
if start >= 0 {
spans = append(spans, span{start, i})
start = -1
}
} else {
// если подсрез пустой, устанавливаем индекс начала текущего подсреза
if start < 0 {
start = i
}
}
// Переходим к следующей кодовой точке.
i += size
}
// если последний подсрез не пустой, сохраняем его индексы в spans
if start >= 0 {
spans = append(spans, span{start, len(s)})
}
// создаем срез подсрезов на основе индексов в spans
a := make([][]byte, len(spans))
for i, span := range spans {
a[i] = s[span.start:span.end:span.end]
}
return a
}
Пример:
func TryFieldsFunc() {
s := []byte("hello, world, golang, bytes")
f := func(r rune) bool {
return unicode.IsSpace(r) || r == ','
}
subslices := bytes.FieldsFunc(s, f)
for _, subslice := range subslices {
fmt.Println(string(subslice))
}
}
Вывод:
hello
world
golang
bytes
Конкатенирует элементы среза s
с разделителем sep
между ними и возвращает новый срез байтов.
func Join(s [][]byte, sep []byte) []byte {
// если длина среза s равна 0, возвращаем пустой срез байтов
if len(s) == 0 {
return []byte{}
}
// если длина среза s равна 1, просто возвращаем копию первого элемента
if len(s) == 1 {
return append([]byte(nil), s[0]...)
}
// вычисляем общую длину результирующего среза байтов.
var n int
// если разделитель sep не пустой, добавляем его длину, умноженную на количество элементов среза s,
// минус 1 (т.к. между последними двумя элементами разделитель не нужен)
if len(sep) > 0 {
if len(sep) >= maxInt/(len(s)-1) {
panic("bytes: Join output length overflow")
}
n += len(sep) * (len(s) - 1)
}
// добавляем длину каждого элемента среза s к общей длине
for _, v := range s {
if len(v) > maxInt-n {
panic("bytes: Join output length overflow")
}
n += len(v)
}
// создаем новый срез байтов длиной n.
b := bytealg.MakeNoZero(n)[:n:n]
// копируем первый элемент среза s в результирующий срез b
bp := copy(b, s[0])
// конкатенируем остальные элементы среза s, разделяя их разделителем sep
for _, v := range s[1:] {
bp += copy(b[bp:], sep)
bp += copy(b[bp:], v)
}
return b
}
Пример использования:
func TryJoin() {
s := [][]byte{{'h', 'e', 'l', 'l', 'o'}, {'w', 'o', 'r', 'l', 'd'}, {'g', 'o', 'l', 'a', 'n', 'g'}}
result := bytes.Join(s, []byte{',', ' '})
fmt.Println(string(result)) // hello, world, golang
}
func HasPrefix(s, prefix []byte) bool {
return len(s) >= len(prefix) && Equal(s[0:len(prefix)], prefix)
}
Проверка начинается ли срез байт с заданного префикса.
func TryPrefix() {
s := []byte("hello, world")
hasPrefix := bytes.HasPrefix(s, []byte("hello"))
fmt.Println(hasPrefix) // true
}
func HasSuffix(s, suffix []byte) bool {
return len(s) >= len(suffix) && Equal(s[len(s)-len(suffix):], suffix)
}
То же самое, но уже суффикс, так что можно без примера.
Функция mapping
должна принимать один аргумент типа rune
(Unicode кодовую точку) и возвращать один аргумент типа rune
. Эта функция будет применена к каждому символу в срезе байтов s
. Если функция mapping
возвращает отрицательное значение, соответствующий символ будет удален из среза байтов без замены. Может быть полезной в различных ситуациях, когда необходимо преобразовать символы в срезе байтов. Например, она может быть использована для преобразования всех символов в верхний или нижний регистр, для удаления непечатных символов или для замены определенных символов другими.
func Map(mapping func(r rune) rune, s []byte) []byte {
b := make([]byte, 0, len(s))
for i := 0; i < len(s); {
wid := 1
r := rune(s[i])
if r >= utf8.RuneSelf {
r, wid = utf8.DecodeRune(s[i:])
}
r = mapping(r)
if r >= 0 {
b = utf8.AppendRune(b, r)
}
i += wid
}
return b
}
Пример:
func TryMap() {
s := []byte("hello, world")
// преобразуем все в верхний регистр
mapping := func(r rune) rune {
if r >= 'a' && r <= 'z' {
return r - 32
}
return r
}
result := bytes.Map(mapping, s)
fmt.Println(string(result)) // HELLO, WORLD
}
func Repeat(b []byte, count int) []byte {
if count == 0 {
return []byte{}
}
if count < 0 {
panic("bytes: negative Repeat count")
}
if len(b) > maxInt/count {
panic("bytes: Repeat output length overflow")
}
n := len(b) * count
if len(b) == 0 {
return []byte{}
}
const chunkLimit = 8 * 1024
chunkMax := n
if chunkMax > chunkLimit {
chunkMax = chunkLimit / len(b) * len(b)
if chunkMax == 0 {
chunkMax = len(b)
}
}
nb := bytealg.MakeNoZero(n)[:n:n]
bp := copy(nb, b)
for bp < n {
chunk := bp
if chunk > chunkMax {
chunk = chunkMax
}
bp += copy(nb[bp:], nb[:chunk])
}
return nb
}
Если длина результирующего среза больше определенного предела (8 КБ), функция ограничивает размер блока, чтобы избежать перегрузки кэша процессора.
Так как нет возможности вывести ошибку при overflow, то мы упадем с паникой
8КБ - эмпирически найденное значение
Разработчики предупреждают, что не надоудалять или изменять сигнатуру типа функции Repeat
, так как это может привести к ошибкам связывания в пакетах, которые используют ее в качестве linkname
Вот дословный комментарий:
// Despite being an exported symbol,
// Repeat is linknamed by widely used packages.
// Notable members of the hall of shame include:
// - gitee.com/quant1x/num
//
// Do not remove or change the type signature.
// See go.dev/issue/67401.
//
// Note that this comment is not part of the doc comment.
//
//go:linkname Repeat
Разберем еще одну интересную функцию, которая преобразует все символы в верхний регистр.
func ToUpper(s []byte) []byte {
// проверяем, является ли срез ASCII-срезом
isASCII, hasLower := true, false
for i := 0; i < len(s); i++ {
c := s[i]
if c >= utf8.RuneSelf {
isASCII = false
break
}
hasLower = hasLower || ('a' <= c && c <= 'z')
}
// если срез является ASCII-срезом, используем оптимизированный алгоритм
if isASCII {
// если в срезе нет строчных символов, просто возвращаем копию
if !hasLower {
return append([]byte(""), s...)
}
// создаем новый срез байтов.
b := bytealg.MakeNoZero(len(s))[:len(s):len(s)]
// преобразуем все строчные символы в верхний регистр.
for i := 0; i < len(s); i++ {
c := s[i]
if 'a' <= c && c <= 'z' {
c -= 'a' - 'A'
}
b[i] = c
}
// возвращаем новый срез байтов
return b
}
// если срез не является ASCII-срезом, используем функцию Map
return Map(unicode.ToUpper, s)
}
Ну и довольно тривиальный пример:
func TryToUpper() {
s := []byte("hello, world!")
result := bytes.ToUpper(s)
fmt.Println(string(result)) // HELLO, WORLD!
}
func ToLower(s []byte) []byte {
// проверяем, состоит ли срез байтов s только из ASCII-символов
isASCII, hasUpper := true, false
for i := 0; i < len(s); i++ {
c := s[i]
if c >= utf8.RuneSelf {
isASCII = false
break
}
hasUpper = hasUpper || ('A' <= c && c <= 'Z')
}
// если срез байтов s состоит только из ASCII-символов, мы можем оптимизировать преобразование
if isASCII {
// если в срезе байтов s нет заглавных букв, мы просто возвращаем копию среза байтов s
if !hasUpper {
r
eturn append([]byte(""), s...)
}
// создаем новый срез байтов b с помощью bytealg.MakeNoZero(), который выделяет память для среза байтов без инициализации нулями
b := bytealg.MakeNoZero(len(s))[:len(s):len(s)]
// проходим по всем символам в срезе байтов s и преобразуем все заглавные буквы ASCII в нижний регистр, добавляя к ним разницу между кодами символов 'a' и 'A'
for i := 0; i < len(s); i++ {
c := s[i]
if 'A' <= c && c <= 'Z' {
c += 'a' - 'A'
}
b[i] = c
}
return b
}
// если срез байтов s содержит не-ASCII символы, мы используем функцию Map() для преобразования всех символов в нижний регистр
return Map(unicode.ToLower, s)
}
Пример использования:
func TryTolower() {
s := []byte("Hello, World!")
lower := bytes.ToLower(s)
fmt.Println(string(lower)) // hello, world!
}
func ToTitle(s []byte) []byte { return Map(unicode.ToTitle, s) }
ToTitle
преобразует все символы в срезе байтов s
в заглавные буквы с помощью функции unicode.ToTitle
. Эта функция использует функцию Map
, которая применяет указанную функцию к каждому символу в срезе байтов s
.
Пример:
func TryToTitle() {
s := []byte("hello, world!")
title := bytes.ToTitle(s)
fmt.Println(string(title)) // HELLO, WORLD!
}
Основное же отличие от ToUpper
заключается в том, что функция ToUpper
преобразует все символы в срезе байтов s
в верхний регистр, то есть в прописные буквы. Функция ToTitle
преобразует все символы в срезе байтов s
в заглавные буквы, то есть в буквы, которые используются для написания заголовков. Эти методы вернут разные кодовые точки, визуально вы не сможеет отличить их.
Наглядный пример:
func TryDifference() {
str := "aáäAÁÄbBcçÇCdz"
// For most characters it seems that ToTitle() and ToUpper() are the same
fmt.Println(strings.ToTitle(str)) // AÁÄAÁÄBBCÇÇCDz
fmt.Println(strings.ToUpper(str)) // AÁÄAÁÄBBCÇÇCDz
// But let's compare the unicode points of the composite character 'dz'
fmt.Println()
str = "dz"
fmt.Printf("%+q", str) // "\u01f3"
fmt.Println()
fmt.Printf("%+q", strings.ToTitle(str)) // "\u01f2"
fmt.Println()
fmt.Printf("%+q", strings.ToUpper(str)) // "\u01f1"
}
Пример взят отсюда.
Функция ToUpperSpecial
использует Map
, которая применяет указанную функцию к каждому символу в срезе байтов s
. В данном случае, функцией, которая применяется к каждому символу, является функция c.ToUpper
, которая преобразует символ в верхний регистр, используя специальные правила преобразования, заданные параметром c
.
func ToUpperSpecial(c unicode.SpecialCase, s []byte) []byte {
return Map(c.ToUpper, s)
}
Пример, чтобы не выдумывать взят из документации, так как это достаточно частный случай:
func main() {
fmt.Println(strings.ToUpperSpecial(unicode.TurkishCase, "örnek iş")) // ÖRNEK İŞ
}
Ровно то же самое, за исключением того, что идет преобразование в нижний регистр.
func ToLowerSpecial(c unicode.SpecialCase, s []byte) []byte {
return Map(c.ToLower, s)
}
func ToTitleSpecial(c unicode.SpecialCase, s []byte) []byte {
return Map(c.ToTitle, s)
}
Еще одна функция из этой же тематики.
func ToValidUTF8(s, replacement []byte) []byte {
// создаем новый срез байтов b с нулевой длиной и емкостью, равной сумме длины s и длины replacement
b := make([]byte, 0, len(s)+len(replacement))
// создаем флаг invalid, который указывает, является ли предыдущий байт недопустимым
invalid := false
// проходим по всем байтам в срезе s
for i := 0; i < len(s); {
// получаем текущий байт
c := s[i]
// если байт является допустимым ASCII-символом,
// добавляем его в срез b и продолжаем проверку следующего байта
if c < utf8.RuneSelf {
i++
invalid = false
b = append(b, c)
continue
}
// получаем длину UTF-8 последовательности, начинающейся с текущего байта
_, wid := utf8.DecodeRune(s[i:])
// если текущий байт является первым байтом недопустимой UTF-8 последовательности,
// проверяем, является ли предыдущий байт недопустимым.
if wid == 1 {
// если предыдущий байт недопустим, добавляем заменитель replacement в срез b.
if invalid {
invalid = false
b = append(b, replacement...)
}
// увеличиваем счетчик байтов и продолжаем проверку следующего байта.
i++
continue
}
// если текущий байт является первым байтом допустимой UTF-8 последовательности,
// добавляем все байты этой последовательности в срез b и продолжаем проверку следующего байта
invalid = false
b = append(b, s[i:i+wid]...)
i += wid
}
// возвращаем срез b, который содержит только допустимые UTF-8 последовательности из исходного среза s
return b
}
Эта функция используется для преобразования среза байтов s
в допустимый UTF-8 формат, заменяя недопустимые последовательности байтов на заданный заменитель replacement
. Функция создает новый срез байтов b
с нулевой длиной и емкостью, равной сумме длины s
и длины replacement
. Затем функция проходит по всем байтам в срезе s
и проверяет, являются ли они допустимыми UTF-8 последовательностями.
Пример:
func TryToValidUTF8() {
s := []byte("\xff\x00hello\x80world")
replacement := []byte("?")
valid := bytes.ToValidUTF8(s, replacement)
fmt.Println(string(valid)) // ?hello?world
}
Функция Title
преобразует все буквы, начинающие слова, в заглавные, считает, что срез байт s
содержит текст, закодированный в UTF-8, и возвращает копию среза, в которой все буквы, начинающие слова, преобразованы в заглавные.
func Title(s []byte) []byte {
prev := ' '
return Map(
func(r rune) rune {
if isSeparator(prev) {
prev = r
return unicode.ToTitle(r)
}
prev = r
return r
},
s)
}
Обратите внимание, что функция Title
устарела и некорректно обрабатывает некоторые символы пунктуации. Рекомендуется использовать пакет golang.org/x/text/cases
для преобразования текста в заглавные буквы.
Вспомогательная функция использует замыкание, чтобы запоминать предыдущий символ, и обновляет его значение после обработки текущего символа.
Но можем обратить внимание на isSeparator
, внутри его лежит проверка, является ли символ разделителем слов.
func isSeparator(r rune) bool {
// алфавитно-цифровые символы ASCII и символ подчеркивания не являются разделителями
if r <= 0x7F {
switch {
case '0' <= r && r <= '9':
return false
case 'a' <= r && r <= 'z':
return false
case 'A' <= r && r <= 'Z':
return false
case r == '_':
return false
}
return true
}
// также буквы и цифры не являются разделителями
if unicode.IsLetter(r) || unicode.IsDigit(r) {
return false
}
// используем пробелы как разделители
return unicode.IsSpace(r)
}
Пример использования:
func TryTitle() {
s := []byte("hello, world!")
title := bytes.Title(s)
fmt.Println(string(title)) // Hello, World!
}
Эта функция удаляет все символы слева от первого символа, для которого функция f
возвращает false
, также считает, что срез байт s
содержит текст, закодированный в UTF-8.
func TrimLeftFunc(s []byte, f func(r rune) bool) []byte {
i := indexFunc(s, f, false)
// если ошибочно, то возращаем -1
if i == -1 {
return nil
}
return s[i:]
}
Пример:
func TryTrimLeftFunc() {
s := []byte(")))hello, world!)))")
trimmed := bytes.TrimLeftFunc(s, func(r rune) bool {
return r == ')'
})
fmt.Println(string(trimmed)) // hello, world!)))
}
В этом примере мы создаем срез байтов s
, содержащий строку ")))hello, world!)))". Затем мы удаляем все скобочки слева от первого непробельного символа с помощью функции bytes.TrimLeftFunc(s, func(r rune) bool { return r == ' ' })
.
Разберем сам indexFunc
.
func indexFunc(s []byte, f func(r rune) bool, truth bool) int {
start := 0
for start < len(s) {
wid := 1
r := rune(s[start])
if r >= utf8.RuneSelf {
r, wid = utf8.DecodeRune(s[start:])
}
if f(r) == truth {
return start
}
start += wid
}
return -1
}
В целом, ничего необычного, это тот же IndexFunc, за исключением того, что если truth==false, то смысл функции-предиката инвертирован
func TrimRightFunc(s []byte, f func(r rune) bool) []byte {
i := lastIndexFunc(s, f, false)
if i >= 0 && s[i] >= utf8.RuneSelf {
_, wid := utf8.DecodeRune(s[i:])
i += wid
} else {
i++
}
return s[0:i]
}
То же самое как и с Left, но уже справа.
func TryTrimRightFunc() {
s := []byte(")))hello, world!)))")
trimmed := bytes.TrimLeftFunc(s, func(r rune) bool {
return r == ')'
})
fmt.Println(string(trimmed)) // hello, world!)))
}
Чтобы статья не растягивалась в воду, существует еще функции по типу: TrimFunc
, TrimPrefix
, TrimSuffix
и выполняют они ровно такую же логику. Также есть реализации Trim
, TrimLeft
, TrimRight
, TrimSpace
, все они построены по одному практически принципу, но со своими особенностями по типу - использование asciiSet
.
func Runes(s []byte) []rune {
t := make([]rune, utf8.RuneCount(s))
i := 0
for len(s) > 0 {
r, l := utf8.DecodeRune(s)
t[i] = r
i++
s = s[l:]
}
return t
}
Возращаем из среза байт срез рун, ничего особенного.
func Replace(s, old, new []byte, n int) []byte {
// m - количество вхождений среза old в срез s
m := 0
// если n не равно нулю, вычисляем количество вхождений
if n != 0 {
m = Count(s, old)
}
// если нет вхождений, возвращаем копию исходного среза
if m == 0 {
return append([]byte(nil), s...)
}
// если n меньше нуля или m меньше n, заменяем все вхождения
if n < 0 || m < n {
n = m
}
// выделяем буфер t длиной len(s) + n*(len(new)-len(old)) байтов
t := make([]byte, len(s)+n*(len(new)-len(old)))
// w - текущая позиция в буфере t
w := 0
// start - текущая позиция в срезе s
start := 0
for i := 0; i < n; i++ {
// j - позиция начала следующего вхождения среза old в срез s
j := start
// если длина среза old равна нулю, вычисляем длину кодовой точки UTF-8
if len(old) == 0 {
if i > 0 {
_, wid := utf8.DecodeRune(s[start:])
j += wid
}
// находим позицию начала следующего вхождения среза old в срез s
} else {
j += Index(s[start:], old)
}
// копируем часть среза s от позиции start до позиции j в буфер t
w += copy(t[w:], s[start:j])
// копируем срез new в буфер t
w += copy(t[w:], new)
// обновляем значение start.
start = j + len(old)
}
// копируем оставшуюся часть среза s в буфер t
w += copy(t[w:], s[start:])
// возвращаем подсрез t[0:w]
return t[0:w]
}
Функция заменят срез на срез в заданное количество раз.
func TryReplace() {
s := []byte("hello, world!")
old := []byte("world")
newB := []byte("Go")
replaced := bytes.Replace(s, old, newB, 1)
fmt.Println(string(replaced)) // hello, Go!
}
Заменяет все вхождения.
func ReplaceAll(s, old, new []byte) []byte {
return Replace(s, old, new, -1)
}
Просто обертка над Replace
.
func EqualFold(s, t []byte) bool {
// этот цикл проверяет, равны ли ASCII-символы в срезах s и t
i := 0
for ; i < len(s) && i < len(t); i++ {
sr := s[i]
tr := t[i]
// если текущий символ не является ASCII-символом, переходим к метке hasUnicode
if sr|tr >= utf8.RuneSelf {
goto hasUnicode
}
// если текущие символы равны, продолжаем цикл
if tr == sr {
continue
}
// если текущие символы не равны, меняем их местами, чтобы sr был меньше tr
if tr < sr {
tr, sr = sr, tr
}
// если sr и tr являются ASCII-символами, проверяем, можно ли преобразовать sr в tr с помощью преобразования регистра
if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
continue
}
// если символы не могут быть преобразованы друг в друга, возвращаем false
return false
}
// если оба среза пусты, возвращаем true
return len(s) == len(t)
hasUnicode:
// если срезы s или t содержат не-ASCII символы, переходим к этой метке
s = s[i:]
t = t[i:]
for len(s) != 0 && len(t) != 0 {
// извлекаем первый символ из каждого среза
var sr, tr rune
if s[0] < utf8.RuneSelf {
sr, s = rune(s[0]), s[1:]
} else {
r, size := utf8.DecodeRune(s)
sr, s = r, s[size:]
}
if t[0] < utf8.RuneSelf {
tr, t = rune(t[0]), t[1:]
} else {
r, size := utf8.DecodeRune(t)
tr, t = r, t[size:]
}
// если символы равны, продолжаем цикл. Если нет, возвращаем false
if tr == sr {
continue
}
// если символы не равны, меняем их местами, чтобы sr был меньше tr
if tr < sr {
tr, sr = sr, tr
}
// если tr является ASCII-символом, проверяем, можно ли преобразовать sr в tr с помощью преобразования регистра
if tr < utf8.RuneSelf {
if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' {
continue
}
return false
}
// в общем случае используем функцию unicode.SimpleFold для преобразования символов в нижний регистр
r := unicode.SimpleFold(sr)
for r != sr && r < tr {
r = unicode.SimpleFold(r)
}
if r == tr {
continue
}
return false
}
// если один из срезов пуст, проверяем, пуст ли другой срез
return len(s) == len(t)
}
Сравнивает 2 среза без учета регистра.
Пример:
func TryEqualFold() {
s := []byte("Hello, World!")
t := []byte("hello, world!")
equal := bytes.EqualFold(s, t)
fmt.Println(equal) // true
}
Казалось бы, за обычным действием скрывается многое.
func Cut(s, sep []byte) (before, after []byte, found bool) {
if i := Index(s, sep); i >= 0 {
return s[:i], s[i+len(sep):], true
}
return s, nil, false
}
Cut
разрезает срез байтов s
вокруг первого вхождения разделителя sep
, возвращая текст до и после разделителя.
Пример:
func TryCut() {
s := []byte("hello, world!")
sep := []byte(", ")
before, after, found := bytes.Cut(s, sep)
fmt.Println(string(before), string(after), found) // hello world! true
}
func Clone(b []byte) []byte {
if b == nil {
return nil
}
return append([]byte{}, b...)
}
Клонирует слайс байт.
func TryClone() {
b := []byte{"hello world"}
c := bytes.Clone(b)
fmt.Println(string(b), string(c), bytes.Equal(b, c)) // hello world! hello world! true
}
func CutPrefix(s, prefix []byte) (after []byte, found bool) {
if !HasPrefix(s, prefix) {
return s, false
}
return s[len(prefix):], true
}
Удаляет заданный префикс.
func TryCutPrefix() {
s := []byte("/path/to/file.txt")
prefix := []byte("/path/to/")
after, found := bytes.CutPrefix(s, prefix)
fmt.Println(string(after), found) // file.txt true
}
func CutSuffix(s, suffix []byte) (before []byte, found bool) {
if !HasSuffix(s, suffix) {
return s, false
}
return s[:len(s)-len(suffix)], true
}
Удаляет заданный суффикс.
func TryCutSuffix() {
s := []byte("file.txt")
suffix := []byte(".txt")
before, found := bytes.CutSuffix(s, suffix)
fmt.Println(string(before), found) // file true
}
Вот и подошла к концу статья, надеюсь я помог немного разобраться с работой пакета bytes
. В целом, как мне кажется, статья получилось достаточно информативной и описывающая практически каждую функцию в пакете. Спасибо за чтение!