Разбираемся вместе: строки в golang

Один из постов серии «Разбираемся вместе«: представляет из себя разбор определенной темы лично мной в целях улучшения понимания и возможно, получения фидбека от более опытных людей.
* Этот пост в совокупности с комментариями может помочь кому-то лучше понять разбираемую тему или сделать более подробный разбор на базе него.
* Этот пост не служит инструкцией, курсом или документацией, где гаранитируется 100% достоверная информация по озвученной теме — автор учится сам, так что ошибки возможны.

Строка (string) — это базовый (basic) тип данных, который представляет из себя неизменяемую последовательность байт, представленных в кодировке UTF-8.

Строки должны быть заключены в двойные кавычки.

  • Одинарные же кавычки представляют отдельный символ, который в Go представлен типом rune (псевдоним int32).

str := "A"

fmt.Println(str) // Выводит A

char := 'A'

fmt.Println(char) // Выводит 65 — символ это в первую очередь код Unicode

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

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

  • Unicode — это также система кодирования символов, которая включает в себя огромный набор символов, каждый из которых имеет свой уникальный код. Эти коды называются Unicode code points.

UTF-8 (8-bit Unicode Transformation Format) — это один из способов (или схем) кодирования символов Unicode в последовательности байт.

  • Это позволяет эффективно хранить символы в памяти и передавать их по сети.

  • При кодировании в формате UTF-8 каждый символ может занимать от 1 до 4 байт, в зависимости от его позиции в стандарте Unicode.

  • Символы из ASCII (U+0000 до U+007F) кодируются одним байтом.

  • Символы из более широких диапазонов Unicode (например, кириллица, китайские иероглифы, эмодзи) могут занимать 2, 3 или 4 байта.

Индексация строк

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

  • При обращении по индексу,  возвращается тип byte.

  • Это отлично работает для символов ASCII, которые занимают 1 байт.

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

Подробнее

Символы английского алфавита (символы ASCII) занимают 1 байт пространства.

  • то есть числовое представление символа ASCII, его код Unicode, умещается в 1 байт.

Поэтому каждый отдельный символ ASCII (вернее его код) можно получить через индекс.

s := “string”

fmt.Println(s[0]) // Выводит 83 — код символа в Unicode тип byte

fmt.Println(string(s[0])) // Выводит s — символ алфавита тип string

  • Делается каст типа byte в строку, чтобы привести символ в человекочитаемый вид.

Символы других алфавитов (или эмодзи) могут занимать более 1 байта. Поэтому через индекс нельзя получить полностью код Unicode. Вернется лишь значение первого байта.

  • Например, символы кириллицы занимают 2 байта пространства — числовое представление символа кириллицы, его код в Unicode не вмещается в 1 байт, поэтому происходит разделение на 2 байта.

s := “Привет”

fmt.Println(s[0]) // вернет Ð — код непонятен для кодировки utf-8, так как первый байт символа содержит лишь часть кода Unicode

  • символ кириллицы занимает 2 байта, через индекс забирается только первый байт, что искажает целевой символ

  • если взять срез fmt.Println(s[0:2]) // возвращается П — забираются первые 2 байта, которые представляют из себя код и код автоматически декодируется в понятный человеку символ.

Как работать с символами строк

Чтобы корректно работать с символами, нужно преобразовать строку в срез типа []rune.

  • Срез []rune представляет строку как последовательность Unicode-кодов символов.

s := “Привет”

runes := []rune(s)

fmt.Println(runes[0]) // 1055 (код символа П в Unicode)

fmt.Println(string(runes[0])) // каст кода П в строку


  • теперь каждый символ кириллицы правильно переведен в тип rune, что представляет код Unicode

Длина строки

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

Если передать строку в функцию len(s), функция вернет размер строки в байтах.

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

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

Для того чтобы получить количество символов (вместо байтов), можно использовать пакет unicode/utf8.

import “unicode/utf8”

s := “Это строка”

fmt.Println(len(s)) // 19

fmt.Println(utf8.RuneCountInString(s)) // 10

Еще один способ получить длину строки (количество символов) это сделать каст строки в срез с элементами типа rune.

s := “Это строка”

fmt.Println(len([]rune(s))) // 10

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

Литералы строк

Литерал — это фиксированное значение, напрямую указанное в исходном коде программы.

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

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

Литерал строки — это текст в исходном коде программы.

  • Кавычки, которые окружают литерал строки, — это синтаксическая часть языка Go, они нужны для обозначения строки.

  • С помощью двойных кавычек, компилятор golang поймет, что он работает со строкой.

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

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

Управляющие символы

Для форматирования строк и работы с особыми символами в Go используются управляющие символы.

Управляющие символы начинаются с обратного слеша (\), за которым следует другой символ, определяющий их действие.

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

Примеры управляющих символов в Go:

  • \n — перевод строки

  • \t — символ табуляции

  • \f — подача страницы

  • \\ — обратный слеш

  • \" — экранирование двойной кавычки

  • \' — экранирование одинарной кавычки

Многострочная печать

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

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

  • В строках, заключенных в обратные апострофы, не требуется экранировать символы. Например, символы \ или кавычки (") можно использовать без обратного слэша.

Такие строки называют "сырыми" (raw string literals), так как они сохраняют текст в его исходном виде без обработки управляющих символов.

text := `This is a

multi-line string.

It preserves line breaks and spaces.`

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

Оператор + используется для соединения двух строк.

s1 := "Hello, "

s2 := "world!"

s3 := s1 + s2

fmt.Println(s3) // "Hello, world!"


Эта операция создаёт новую строку, которая является результатом соединения исходных строк.

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

Пакет strings

Встроенный пакет, который предоставляет функции для работы со строками в кодировке utf-8.

  • с полным набором функций можно ознакомиться в документации пакета, тут разберем некоторые

strings.Contains()

Эта функция проверяет, содержится ли подстрока в строке.

  • Возвращает true, если строка найдена, и false — если не найдена.

import "strings"

s := "hello world"

fmt.Println(strings.Contains(s, "world")) // true

fmt.Println(strings.Contains(s, "go"))  // false

strings.ToUpper() и strings.ToLower()

Эти функции позволяют преобразовать строку в верхний или нижний регистр.

import "strings"

s := "Hello"

fmt.Println(strings.ToUpper(s)) // "HELLO"

fmt.Println(strings.ToLower(s)) // "hello"

strings.Trim()

Функция strings.Trim() позволяет удалить указанный вторым параметром символы с начала и конца строки.

import "strings"

s := "  Hello, world!  "

fmt.Println(strings.Trim(s, " ")) // Выведет "Hello, world!"

strings.TrimSpace()

Можно также использовать strings.TrimSpace(), чтобы удалить только пробелы.

strings.Split()

Функция strings.Split() разделяет строку на подстроки по заданному разделителю и возвращает срез строк.

import "strings"

s := "a b c d"

result := strings.Split(s, " ")

fmt.Println(result) // [a b c d]

strings.Join()

Функция strings.Join() объединяет срез строк в одну строку с заданным разделителем.

import "strings"

slice := []string{"a", "b", "c", "d"}

result := strings.Join(slice, "")

fmt.Println(result) // "abcd"

strings.Replace()

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

import "strings"

s := "hello world"

result := strings.Replace(s, "world", "Go", -1)

fmt.Println(result) // "hello Go"

  • -1 все вхождения

range

range — это ключевое слово для перебора коллекций в цикле.

При каждом проходе цикла range возвращает два значения:

  • индекс начального байта символа строки

  • индекс элемента (массив или срез)

  • ключ при работе с картами

  • и затем соответствующее значение из коллекции.

Со строками ключевое слово range используется для итерации и извлечения каждого символа (rune) из строки.

  • Это позволяет корректно обрабатывать многобайтовые символы в строках.


s := "string"

for i, r := range s {

fmt.Printf("%d: %c\n", i, r)

}

  • В этом примере i — это начальный индекс байта в строке, а r — это сам символ (rune).

fmt.Sprintf()

Аналог fmt.Printf(), но результат сохраняется в строку, а не выводится в консоль.

str := fmt.Sprintf("Hello, %s!", "world")

fmt.Println(str)