Струны Голанга неизменны. В общем, о неизменяемых данных проще рассуждать, но это также означает, что ваша программа должна выделить больше памяти, чтобы «изменить» эти данные. Иногда ваша программа не может позволить себе такую ​​роскошь. Например, может не хватить памяти для выделения. Другая причина: вы не хотите создавать дополнительную работу для сборщика мусора.

В C строка представляет собой последовательность символов с завершающим нулем - char*. Каждый char представляет собой отдельный байт, и строка продолжается до тех пор, пока не появится символ '\0'. Если вы указали произвольный участок памяти и назвали его строкой C, вы увидите каждый байт по порядку, пока не дойдете до нуля.

В Go string - это отдельный тип данных. По сути, это все еще последовательность байтов, но:

  • Это фиксированная длина. Это не продолжается до тех пор, пока не появится ноль.
  • В нем есть дополнительная информация: его длина.
  • «Символы» или runes могут занимать несколько байтов.
  • Это непреложно.

Итак, 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