Share This
Связаться со мной
Крути в низ
Categories
//Go: Строки, руны, байты. Пакет strings. Хеш-таблица (map)

Go: Строки, руны, байты. Пакет strings. Хеш-таблица (map)

go stroki runy bajty paket strings hesh tablica map bbc440b - Go: Строки, руны, байты. Пакет strings. Хеш-таблица (map)

Энтузиаст-разработчик, автор статей по программированию. Сфера интересов — backend, web 3.0, кибербезопасность. Ранее в уроке про типы данных мы познакомились со строками, рунами и байтами. В этой статье расширим наши знания об этих типах, рассмотрим пакет strings и подробно изучим хеш-таблицы.

go stroki runy bajty paket strings hesh tablica map 2039039 - Go: Строки, руны, байты. Пакет strings. Хеш-таблица (map)

← Часть 7 Массивы и слайсы. Append и сopy. Пакет slices

Немного о кодировках

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

На данный момент стандартом является кодировка Unicode из 1 114 112 позиций, разделенных на 17 блоков по 65 536 символов. Все они записываются в шестнадцатеричном представлении с приставкой «U+». Например, латинская буква «a» имеет код «U+0061», русская буква «я» записана как «U+044F».

Исходный код Go определяется как текст в кодировке UTF-8 (Unicode Transformation Format, 8-bit) – стандарт компактного хранения символов Unicode, обладающий обратной совместимостью с ASCII и переменной длиной. Это дает возможность закодировать определенные символы разным количеством байтов. К примеру, в UTF-8 латинский алфавит закодирован одним байтом, а кириллица – двумя. Данная особенность будет иметь значение при вычислении длины строк, что мы увидим в продолжение статьи.

Для более подробного ознакомления с кодировками рекомендуем прочитать статью «Как работают кодировки текста».

👨‍💻 Библиотека Go разработчика Больше полезных материалов вы найдете на нашем телеграм-канале«Библиотека Go разработчика» 🎓 Библиотека Go для собеса Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса» 🧩 Библиотека задач по Go Интересные задачи по Go для практики можно найти на нашем телеграм-канале«Библиотека задач по Go»

Строки

Байты и руны

Тип данных byte в Go представляет псевдоним для беззнакового 8-битного целочисленного типа uint8. Он принимает значения от 0 до 255, поэтому используются для представления ASCII-символов. Слайсы байт широко применяются в различных библиотеках за счет своей производительности и гибкости.

Тип данных rune определяется как псевдоним для типа int32. Руна соответствует коду символа в таблице Unicode. Так, тип символа µ есть руна со значением 0x00b5.

Строковые литералы

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

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

Интерпретируемые литералы — это символы, заключенные в двойные кавычки вида «». Текст внутри кавычек представляет собой кодировку UTF-8 отдельных символов. Например, символ µ и его записи xc2xb5, u00b5, U000000B5 определяют два байта 0xc2 0xb5 символа U+00B5 (µ) в кодировке UTF-8:

         symbol := "µ" fmt.Println("\xc2\xb5") // µ fmt.Println("\u00b5") // µ fmt.Println("\U000000B5") // µ fmt.Printf("%x", symbol) // c2b5      

Необработанные (сырые) литералы – это символы, заключенные в двойные кавычки вида «. Их значением является строка из неявно закодированных в UTF-8 символов. В необработанных строковых литералах, в отличие от интерпретируемых, перевод строки осуществляется явно с использованием enter, а символ n не имеет специального значения.

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

Строки

Строки в Go представляют собой неизменяемую последовательность байтов. Содержимое строки есть не что иное, как слайс байтов ([]byte). Именно по этой причине обращение по индексу строки вернет не символ, а его юникод-значение в десятичной системе счисления:

         rusText := "текст" fmt.Println(rusText[0], rusText[1]) // 209 130  // 209 = D1 и 130 = 82 в 16-ричном представлении, // D1 82 является кодом буквы "т" в UTF-8      

Срез [i:j] возвращает новую строку, которая состоит из байтов исходной, начиная с индекса i и заканчивая индексом j, но исключая его:

         str := "текст" fmt.Println(str[0:2]) // т fmt.Println(str[0:1]) // �      

В последней строке вывода можно заметить странный символ. Дело в том, что функция fmt.Println пытается декодировать часть строки str[0:1], но получает один единственный байт 0xd1, который не соответствует ни единому символу в UTF-8. Поэтому в результате будет выведен специальный заменяющий символ с кодом 0xfffd.

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

         str1 := "Букварь" str2 := "Буква" fmt.Println(str1 > str2) // true  str1 = "Кот" str2 = "кот" fmt.Println(str1 == str2) // false  str1 = "код" str2 = "кот" fmt.Println(str1 > str2)  // false fmt.Println([]byte(str1)) // [208 186 208 190 **208** 180] fmt.Println([]byte(str2)) // [208 186 208 190 **209** 130]      

Поддерживаются также основные операции со строками – конкатенация и интерполяция.

Конкатенация – это операция слияния нескольких объектов (чаще всего строк) в один.

Интерполяция – это операция замены заполнителей строки соответствующими значениями.

Как и во многих других языках программирования, конкатенация строк в Go реализуется с помощью оператора +:

         // пример конкатенации строк в Go str1 := "Привет, " str2 := "мир!" fmt.Println(str1 + str2) // Привет, мир!      

Интерполяция в Go реализована немного иначе, чем в динамических языках. Она осуществляется с использованием функций пакета fmt:

         // пример интерполяции строк в Go hour := 12 minute := 35 second := 50 time := fmt.Sprintf("Время %d:%d:%d", hour, minute, second) fmt.Printf("Дата %d-%d-%d\n", 28, 12, 2023) // Дата 28-12-2023 fmt.Println(time) // Время 12:35:50      

Статья по теме 🏃 Готовимся к интервью по Golang: массивы, слайсы и строки

Итерация по рунам

Цикл for-range по строке на каждой итерации декодирует одну руну в UTF-8:

         rusText := "текст" for idx, char := range rusText { 	fmt.Printf("Руна: %#U с индексом %d\n", char, idx) } fmt.Println("Длина строки:", len(rusText))      

На выходе получим Unicode представление каждого символа строки и его индекс:

         Руна: U+0442 'т' с индексом 0 Руна: U+0435 'е' с индексом 2 Руна: U+043A 'к' с индексом 4 Руна: U+0441 'с' с индексом 6 Руна: U+0442 'т' с индексом 8 Длина строки: 10      

Заметим, что каждая руна здесь занимает два байта, а так как функция len() возвращает количество байтов, занимаемых строкой, а не количество символов, то длина строки в два раза больше ожидаемой – 10 вместо 5. В начале статьи мы уже упоминали о том, что в UTF-8 кириллица занимает два байта, а так как язык Go использует эту кодировку, то на выходе получаем вполне обоснованный результат.

С латинскими символами всё немного иначе:

         engText := "text" for idx, char := range engText { 	fmt.Printf("Руна %#U с индексом %d\n", char, idx) } fmt.Println("Длина строки:", len(engText))      

На выходе получим ожидаемые значения индексов и длины:

         Руна U+0074 't' с индексом 0 Руна U+0065 'e' с индексом 1 Руна U+0078 'x' с индексом 2 Руна U+0074 't' с индексом 3 Длина строки: 4      

В общем случае для получения корректной длины строки следует предварительно преобразовать её в слайс рун или же воспользоваться функцией utf8.RuneCountInString() из пакета unicode/utf8:

         str := "текст" ln := len(str) // 10 correctLen1 := utf8.RuneCountInString(str) // 5 correctLen2 := len([]rune(str)) // 5      

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

Итерация по байтам (индексам)

Как было рассмотрено ранее, обращение напрямую по индексам строки выдаст байтовое представление символов. Это поведение сохраняется при итерации:

         rusText := "текст" for i := 0; i < len(rusText); i++ { 	fmt.Printf("%v ", rusText[i]) } fmt.Println() // цикл для перевода 10-ричных значений байтов в 16-ричные for i := 0; i < len(rusText); i++ { 	fmt.Printf("%x ", rusText[i]) }      

В выводе на верхней строке показано десятичное представление нижних 16-ричных значений символов строки в UTF-8:

         209 130 208 181 208 186 209 129 209 130  d1  82  d0  b5  d0  ba  d1  81  d1  82      

Чтобы вывести корректные символы строки, нужно воспользоваться функцией fmt.Printf и спецификатором %c:

         str := "текст" for _, sym := range str { 	fmt.Printf("%c ", sym) // т е к с т }      

Пакет strings

Пакет string стандартной библиотеки содержит полезные функции для работы со строками. Давайте рассмотрим некоторые из них:

  • Подсчет вхождений символа в строку – strings.Count(s, substr string) int:
         str := "привет, мир!" strings.Count(str, "и") // 2      
  • Замена символов – strings.Replace(s, old, new string, n int) string:
         // последний аргумент - количество замен // если он < 0, то лимита на количество замен нет str := "привет, мир!!!" a := strings.Replace(str, "!", "?", -1) // "привет, мир???" b := strings.Replace(a, "?", "!", 2)    // "привет, мир!!?"      
  • Разбиение символов по разделителю sep – strings.Split(s, sep string) []string:
         str := "привет, мир!!!" a := strings.Split(str, ",") // [привет  мир!!!] fmt.Println(a[0]) // привет      
  • Слияние строк с разделителем sep – strings.Join(elems []string, sep string) string:
         str := []string{"01", "01", "2024"} strings.Join(str, "-") // 01-01-2024      
  • Преобразование символов к нижнему или верхнему регистру – strings.ToLower(s string) или strings.ToUpper(s string):
         str := "Просто Строка" strings.ToLower(str) // просто строка strings.ToUpper(str) // ПРОСТО СТРОКА      

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

         // количество итераций может быть в разы больше sb := strings.Builder{} for i := 0; i < 8; i++ { 	sb.WriteString("q") } sb.WriteString("end") fmt.Println(sb.String()) // qqqqqqqqend      

Хеш-таблица

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

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

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

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

go stroki runy bajty paket strings hesh tablica map 84f258f - Go: Строки, руны, байты. Пакет strings. Хеш-таблица (map)

Наглядная иллюстрация хеш-таблицы

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

Существует два основных метода разрешения коллизий:

  • Метод списков

Данный метод заключается в связывании конфликтующих значений в цепочку-список. В таком случае время поиска в худшем случае будет пропорционально длине списка. Наихудшей ситуацией является хеширование всех n ключей к одной ячейке.

  • Метод открытой адресации

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

Хеш-таблица (map) в Go

В рамках данной статьи мы не будем углубляться в детали реализации хеш-таблиц в Go. Для полного погружения в тему рекомендуем прочитать следующие материалы: исходный код map, Effective Go: Maps, Мапы в Go: уровень Pro.

Хеш-таблица в Go (также её называют map, мапа, карта) определяется ключевым словом map и создается одним из следующих способов, где keys_type – тип данных ключей, values_type – тип данных значений:

  • Предпочтительный способ – с использованием make:
         mp := make(map[keys_type]values_type)      
  • С помощью ключевого слова var создается nil-map:
         var mp map[keys_type]values_type      

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

         var mp map[string]int mp["key"] = 1 // panic: assignment to entry in nil map      
  • С указанием элементов в скобках:
         mp := map[keys_type]values_type{ 	key1: val1, 	key2: val2, }      

При этом стоит помнить, что ключами могут быть только сравнимые (comparable) типы данных: числовые (int, float, complex), string, bool, указатель, интерфейс, канал и структуры или массивы, которые содержат значения только этих типов.

Научились создавать мапу, а теперь рассмотрим основные операции над ней:

  1. Вставка: mp[key] = value
  2. Удаление: delete(mp, key)
  3. Поиск: value := mp[key]

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

         mp := map[int]string{ 	1: "a", } value := mp[2] fmt.Println(value) // ""     

Но стоит помнить, что возвращаемое значение будет nil, если нулевое значение типа также является nil.

Для корректной проверки наличия элемента в мапе используется множественное присваивание. В этом случае возвращается пара – само значение и булевая переменная. Если она равна true, это означает присутствие искомого элемента в мапе, иначе – его отсутствие:

         mp := map[int]string{ 	1:"a", } if _, ok := mp[2]; !ok { 	fmt.Println("Не найдено") // Не найдено }     

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

         mp := map[int]string{ 	0: "a", 	1: "b", 	2: "c", 	3: "d", } for i := range mp { 	fmt.Println(mp[i]) }      

Передача мапы в функцию

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

Для примера создадим функцию changeMap, которая будет принимать в качестве параметра мапу и изменять в ней значение:

         func changeMap(map2 map[int]string) { 	map2[1] = "d" }  func main() { 	mp := map[int]string{ 		0: "a", 		1: "b", 		2: "c", 	} 	changeMap(mp) 	fmt.Println(mp) // map[0:a 1:d 2:c] }      

В результате можем увидеть, что значение по ключу 1 изменило своё значение.

Не стоит думать, что мапа передалась по ссылке, так как в Go такое поведение отсутствует. Всё дело в том, что мапа в Go является указателем на структуру hmap:

         // A header for a Go map. type hmap struct { 	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go. 	// Make sure this stays in sync with the compiler's definition. 	count     int // # live cells == size of map.  Must be first (used by len() builtin) 	flags     uint8 	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items) 	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details 	hash0     uint32 // hash seed  	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. 	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing 	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)  	extra *mapextra // optional fields }      

Давайте немного изменим код, чтобы map2 в функции changeMap объявлялась с помощью make:

         func changeMap(map2 map[int]string) { 	map2 = make(map[int]string) 	map2[1] = "d" 	fmt.Println("Мапа map2 в changeMap:", map2) }  func main() { 	mp := map[int]string{ 		0: "a", 		1: "b", 		2: "c", 	} 	changeMap(mp) 	fmt.Println("Мапа mp в main:", mp) // map[0:a 1:d 2:c] }      

В выводе можно заметить, что map2 в changeMap отличается от mp в main:

         Мапа map2 в changeMap: map[1:d] Мапа mp в main: map[0:a 1:b 2:c]      

В этом случае мапа mp передается по значению в changeMap, поэтому не меняется. Все изменения коснутся её локальной копии map2, так как создание мапы с помощью make приводит к инициализации hmap.

Подведём итоги

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

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

***

Содержание самоучителя

  1. Особенности и сфера применения Go, установка, настройка
  2. Ресурсы для изучения Go с нуля
  3. Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
  4. Переменные. Типы данных и их преобразования. Основные операторы
  5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
  6. Функции и аргументы. Области видимости. Рекурсия. Defer
  7. Массивы и слайсы. Append и сopy. Пакет slices
  8. Строки, руны, байты. Пакет strings. Хеш-таблица (map)

  • 0 views
  • 0 Comment

Leave a Reply

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

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

Связаться со мной
Close