Струны Голанга неизменны. В общем, о неизменяемых данных проще рассуждать, но это также означает, что ваша программа должна выделить больше памяти, чтобы «изменить» эти данные. Иногда ваша программа не может позволить себе такую роскошь. Например, может не хватить памяти для выделения. Другая причина: вы не хотите создавать дополнительную работу для сборщика мусора.
В C строка представляет собой последовательность символов с завершающим нулем - char*
. Каждый char
представляет собой отдельный байт, и строка продолжается до тех пор, пока не появится символ '\0'
. Если вы указали произвольный участок памяти и назвали его строкой C, вы увидите каждый байт по порядку, пока не дойдете до нуля.
В Go string
- это отдельный тип данных. По сути, это все еще последовательность байтов, но:
- Это фиксированная длина. Это не продолжается до тех пор, пока не появится ноль.
- В нем есть дополнительная информация: его длина.
- «Символы» или
rune
s могут занимать несколько байтов. - Это непреложно.
Итак, string
в Go имеет некоторую дополнительную структуру по сравнению с char*
в C. Как он это делает? На самом деле это структура:
type StringHeader struct { Data unsafe.Pointer Len int }
Data
здесь аналогична строке C, а Len
- длина. Структура структурной памяти Golang начинается с последнего поля, поэтому, если вы посмотрите на string
под микроскопом, вы сначала увидите Len
, а затем указатель на содержимое string
. (Вы можете найти документацию по этим структурам заголовков в пакете reflect
.)
Прежде чем мы начнем проверять строки, глядя на их StringHeader
поля, как нам в первую очередь преобразовать string
в StringHeader
? Если вам действительно нужно преобразовать один тип Go в другой, используйте пакет unsafe
:
import ( "unsafe" ) s := "hello" header := (*StringHeader)(unsafe.Pointer(&s))
unsafe.Pointer
- нетипизированный указатель. Он может указывать на любую ценность. Это способ сказать компилятору: «Отойди. Я знаю, что я делаю." В данном случае мы преобразуем *string
в unsafe.Pointer
в *StringHeader
.
Теперь у нас есть доступ к базовому представлению string
. Вы когда-нибудь задумывались, как работает len("hello")
? Мы можем реализовать это сами:
func strLen(s string) int { header := (*StringHeader)(unsafe.Pointer(&s) return header.Len }
Получить длину строки - это хорошо, но как насчет ее настройки? Вот что произойдет, если мы искусственно увеличим длину строки:
s := "hello" header := (*StringHeader)(unsafe.Pointer(&s)) header.Len = 100 // cast the header back to 'string' and print it fmt.Print(*(*string)(unsafe.Pointer(header))) /* on stdout: helloint16int32int64panicslicestartuint8write (MB) Value addr= code= ctxt: curg= list= m->p= p->m= */
Изменяя поле Len
заголовка строки, мы можем расширить строку, включив в нее другие части памяти. Наблюдать за этим поведением интересно, но на самом деле это не то, что вам нужно.
Data :: unsafe.Pointer
Вы могли заметить, что StringHeader
имеет поле unsafe.Pointer
, которое указывает на последовательность байтов строки. []byte
также имеет последовательность байтов. Фактически, мы можем построить []byte
из этого указателя. Вот как на самом деле выглядит срез:
type SliceHeader struct { Data unsafe.Pointer Len int Cap int }
Он очень похож на StringHeader
, за исключением того, что в нем есть поле Cap
(вместимость). Что произойдет, если мы построим SliceHeader
из полей StringHeader
?
func strToBytes(s string) []byte { header := (*StringHeader)(unsafe.Pointer(&s)) bytesHeader := &SliceHeader{ Data: header.Data, Len: header.Len, Cap: header.Len, } return *(*[]byte)(unsafe.Pointer(bytesHeader)) } fmt.Print(strToBytes("hello")) // [104 101 108 108 111]
Мы преобразовали string
в []byte
. Так же легко пойти и в другом направлении:
func bytesToStr(b []byte) string { header := (*SliceHeader)(unsafe.Pointer(&b)) strHeader := &StringHeader{ Data: header.Data, Len: header.Len, } return *(*string)(unsafe.Pointer(strHeader)) } fmt.Print(bytesToStr([]byte{104, 101, 108, 108, 111}) // "hello"
Заголовки string
и []byte
используют один и тот же указатель Data
, поэтому они совместно используют память. Если вам когда-нибудь понадобится преобразовать между string
и []byte
, но недостаточно памяти для выполнения копирования, это может быть полезно.
Однако небольшое предостережение: string
должно быть неизменным, а []byte
- нет. Если вы приведете string
к []byte
и попытаетесь изменить массив байтов, это будет ошибкой сегментации.
s := "hello" b := strToBytes(s) b[0] = 100 // panic: runtime error: invalid memory address or nil pointer dereference // [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xd56a2]
Приведение в другом направлении не вызывает ошибки сегментации, но тогда ваш предположительно неизменяемый string
может измениться:
b := []byte{104, 101, 108, 108, 111} s := bytesToStr(b) fmt.Print(s) // "hello" b[0] = 100 fmt.Print(s) // "dello"
Попробуй это
Это небольшое введение в то, что вы можете делать с unsafe.Pointer
, и некоторые знания о базовом представлении типов Go. Если вы хотите поиграть с кодом из этого сообщения (и substr
реализацией), загляните на онлайн-площадку Go Playground здесь: play.golang.org/p/PAjwbct_ohF