Skip to content

Строки в Go

В Go строка по сути является неизменяемой последовательностью байтов (byte sequence). "Последовательность байтов" означает, что нижележащие данные строки состоят из упорядоченных байтов, занимающих непрерывное пространство памяти.

Литералы

Ранее упоминалось, что строки имеют два способа записи литералов: обычные строки и нативные строки.

Обычные строки

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

go
"这是一个普通字符串\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"
这是一个普通字符串
abcdefghijlmn
opqrst  \uvwxyz

Нативные строки

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

go
`这是一个原生字符串,换行
  tab 缩进,\t制表符但是无效,换行
  "这是一个普通字符串"

  结束
`
这是一个原生字符串,换行
        tab 缩进,\t制表符但是无效,换行
        "这是一个普通字符串"

        结束

Доступ

Поскольку строка по сути является последовательностью байтов, операция индексации str[i] возвращает i-й байт. Синтаксис аналогичен срезам. Например, доступ к первому элементу строки:

go
func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

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

116

Разрезание строки:

go
func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

Попытка изменить элемент строки:

go
func main() {
   str := "this is a string"
   str[0] = 'a' // не скомпилируется
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

Хотя изменить строку нельзя, можно её перезаписать:

go
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

Преобразование

Строки можно преобразовать в байтовые срезы, а байтовые срезы или последовательности — в строки:

go
func main() {
   str := "this is a string"
   // Явное преобразование в байтовый срез
   bytes := []byte(str)
   fmt.Println(bytes)
   // Явное преобразование в строку
   fmt.Println(string(bytes))
}

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

go
func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // Изменение байтового среза
  bytes = append(bytes, 96, 97, 98, 99)
    // Присваивание исходной строке
  str = string(bytes)
  fmt.Println(str)
}

После преобразования строки в байтовый срез между ними нет связи. Go выделяет новую область памяти для байтового среза и копирует туда содержимое строки. Изменения байтового среза не влияют на исходную строку — это сделано для безопасности памяти.

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

go
func main() {
  s1 := "hello world"
  b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
  fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}
0xe27bb2 0xe27bb2

Длина

Длина строки — это не количество символов, а длина последовательности байтов. В большинстве случаев мы работаем с символами ASCII, каждый из которых занимает один байт, поэтому длина в байтах совпадает с количеством символов. Для получения длины строки используется встроенная функция len:

go
func main() {
   str := "this is a string" //看起来长度是 16
   str2 := "这是一个字符串" //看起来长度是 7
   fmt.Println(len(str), len(str2))
}
16 21

Китайская строка выглядит короче английской, но фактическая длина больше. Это связано с тем, что в кодировке unicode один китайский иероглиф в большинстве случаев занимает 3 байта, а один английский символ — только 1 байт. Вывод первого элемента строки показывает:

go
func main() {
   str := "this is a string"
   str2 := "这是一个字符串"
   fmt.Println(string(str[0]))
   fmt.Println(string(str2[0]))
   fmt.Println(string(str2[0:3]))
}
t // буква t
è // "фрагмент" китайского иероглифа (первый байт), случайно совпадает с кодировкой итальянского символа è
这 // китайский иероглиф

Копирование

Подобно копированию срезов массивов, копирование строк — это копирование байтовых срезов с использованием встроенной функции copy:

go
func main() {
   var dst, src string
   src = "this is a string"
   desBytes := make([]byte, len(src))
   copy(desBytes, src)
   dst = string(desBytes)
   fmt.Println(src, dst)
}

Также можно использовать функцию strings.Clone, хотя внутренняя реализация примерно одинакова:

go
func main() {
   var dst, src string
   src = "this is a string"
   dst = strings.Clone(src)
   fmt.Println(src, dst)
}

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

Конкатенация строк выполняется оператором +:

go
func main() {
   str := "this is a string"
   str = str + " that is a int"
   fmt.Println(str)
}

Также можно преобразовать в байтовый срез и добавить элементы:

go
func main() {
   str := "this is a string"
   bytes := []byte(str)
   bytes = append(bytes, "that is a int"...)
   str = string(bytes)
   fmt.Println(str)
}

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

go
func main() {
   builder := strings.Builder{}
   builder.WriteString("this is a string ")
   builder.WriteString("that is a int")
   fmt.Println(builder.String())
}
this is a string that is a int

Итерация

В начале статьи упоминалось, что строка в Go — это байтовый срез только для чтения, то есть единицей строки является байт, а не символ. Это часто создаёт проблемы при итерации. Пример:

go
func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

В примере выводятся десятичное и шестнадцатеричное значения байтов:

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!

Поскольку все символы в примере — ASCII, каждый байт соответствует одному символу. Но при наличии не-ASCII символов результат отличается:

go
func main() {
  str := "hello 世界!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

Обычно один китайский иероглиф занимает 3 байта, поэтому результат может быть таким:

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,–
231,e7,ç
149,95,•
140,8c,Œ
33,21,!

Итерация по байтам разбивает китайские иероглифы, что приводит к кракозябрам. Строки в Go явно поддерживают UTF-8. Для корректной обработки используется тип rune. При использовании for range единицей итерации по умолчанию является rune:

go
func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

Вывод:

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!

rune по сути является псевдонимом типа int32. Диапазон набора символов Unicode находится между 0x0000 и 0x10FFFF, максимум три байта. Максимальное количество байтов в легитимной кодировке UTF-8 — 4 байта, поэтому использование int32 оправдано. Преобразование строки в []rune и последующая итерация работают аналогично:

go
func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

Также можно использовать инструменты из пакета utf8:

go
func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

Оба примера дают одинаковый вывод.

TIP

Подробнее о строках можно узнать в статье Strings, bytes, runes and characters in Go.

Golang by www.golangdev.cn edit