Пост из канала Thank Go!

Начало истории

Вернёмся к нашим любимым строкам. В прошлый раз мы разобрались как работают слайсы в Го, а теперь давайте вернёмся к утверждению, что «строки — это слайсы байт».

Возьмём строку в разных представлениях (в байтах и рунах) и попробуем её изменить.

s := "hello"
b := []byte(s)
r := []rune(s)

b[0] = 98 // 'b'
r[0] = 'r'

fmt.Printf("%s %s %s\n", s, string(b), string(r))

>>  hello bello rello

А если изменить что-то в строке?

s[0] = "s"

>> cannot assign to s[0]
>> Go build failed.

Компилятор ругается на присваивание элемента строке. Т.е. не такой уж это и слайс? На этот вопрос на поможет ответить понимание самой внутренней структуры слайса и строки, посмотрим на неё при помощи рефлексии:

sHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&r))

>> &reflect.StringHeader{Data:0x4c1fd1, Len:5}
>> &reflect.SliceHeader{Data:0xc00002c008, Len:5, Cap:8}
>> &reflect.SliceHeader{Data:0xc000014020, Len:5, Cap:8}

Видно, что у настоящих слайсов есть ёмкость (Cap), а у строки — нет. На самом деле строки являются неизменяемыми слайсами. А при конвертации в слайсы байт и рун происходит копирование данных под капотом.

s1 := "hello"
b := []byte(s)
s2 := string(b)
fmt.Printf("%p %p\n", &s1, &s2)

>> 0xc00009e240 0xc00009e250

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

🌶 Для тех кто любит по острее

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

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

В Го такая возможность есть. И если вам нужно всё-таки сделать изменяемую строку, то вот она:

b := []byte{'h', 'e', 'l', 'l', 'o'}
s := *(*string)(unsafe.Pointer(&b))
fmt.Printf("%T %s\n", s)

>> string hello

Всё честно, это строка. А теперь следите за руками:

b[0] = 's'
fmt.Println(s)

>> sello

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

fmt.Printf("%#v\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
fmt.Printf("%#v\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)

>> 0xc00002c008
>> 0xc00002c008

Внимание! Так делать очень опасно и не рекомендуется. Пакет unsafe отбрасывает систему типов Го и лезет напрямую в память. Это может приводить к очень страшным рантайм ошибкам.

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

s = "hello"
b = *(*[]byte)(unsafe.Pointer(&s))
b[0] = 's'

>> unexpected fault address 0x4c1db5
>> fatal error: fault