string
string é um tipo de dados básico muito comum em Go, e foi o primeiro tipo de dados que entrei em contato na linguagem Go.
package main
import "fmt"
func main() {
fmt.Println("hello,world!")
}Acredito que a maioria das pessoas já digitou este código quando começou a aprender Go. Em builtin/builtin.go há uma descrição simples sobre string:
// string é o conjunto de todas as strings de bytes de 8 bits, convencionalmente mas não
// necessariamente representando texto codificado em UTF-8. Uma string pode ser vazia, mas
// não nil. Valores do tipo string são imutáveis.
type string stringDesta descrição podemos obter as seguintes informações:
stringé uma coleção de bytes de 8 bits- O tipo
stringé normalmente codificado emUTF-8 stringpode ser vazia, mas nuncanilstringé imutável
Essas características já são bem conhecidas para quem usa Go frequentemente. Abaixo veremos algo diferente.
Estrutura
Em Go, strings são representadas em tempo de execução pela estrutura runtime.stringStruct, mas ela não é exposta publicamente. Como alternativa, pode-se usar reflect.StringHeader.
TIP
Embora StringHeader tenha sido descontinuado na versão go1.21, ele é muito intuitivo e ainda será usado nas explicações abaixo, sem prejudicar o entendimento. Para mais detalhes, veja Issues · golang/go (github.com).
// runtime/string.go
type stringStruct struct {
str unsafe.Pointer
len int
}
// reflect/value.go
type StringHeader struct {
Data uintptr
Len int
}Os campos são definidos da seguinte forma:
Data, é um ponteiro para o endereço inicial da memória da stringLen, número de bytes da string
Abaixo está um exemplo de acesso ao endereço da string através de ponteiro unsafe:
func main() {
str := "hello,world!"
h := *((*reflect.StringHeader)(unsafe.Pointer(&str)))
for i := 0; i < h.Len; i++ {
fmt.Printf("%s ", string(*((*byte)(unsafe.Add(unsafe.Pointer(h.Data), uintptr(i)*unsafe.Sizeof(str[0]))))))
}
}No entanto, Go agora recomenda usar unsafe.StringData como substituto:
func main() {
str := "hello,world!"
ptr := unsafe.Pointer(unsafe.StringData(str))
for i := 0; i < len(str); i++ {
fmt.Printf("%s ", string(*((*byte)(unsafe.Add(ptr, uintptr(i)*unsafe.Sizeof(str[0]))))))
}
}Ambas as saídas são iguais:
h e l l o , w o r l d !
A string é essencialmente uma área de memória contínua, onde cada endereço armazena um byte. Em outras palavras, é um array de bytes. O resultado obtido pela função len é o número de bytes, não o número de caracteres na string. Isso é especialmente importante quando a string contém caracteres não ASCII.
A string em si ocupa pouca memória, apenas um ponteiro para os dados reais. Isso torna o custo de transferência de strings muito baixo. Pessoalmente, como mantém apenas uma referência de memória, se pudesse ser modificada livremente, seria difícil saber se os dados originais ainda são os desejados (seria necessário usar reflexão ou o pacote unsafe). A menos que os usuários dos dados antigos nunca mais precisem dessa string após o uso. Outra vantagem é que é naturalmente seguro para concorrência, pois ninguém pode modificá-la em circunstâncias normais.
Concatenação

A sintaxe de concatenação de strings é mostrada abaixo, usando diretamente o operador +:
var (
hello = "hello"
dot = ","
world = "world"
last = "!"
)
str := hello + dot + world + lastA operação de concatenação é completada em tempo de execução pela função runtime.concatstrings. Para concatenação de literais como abaixo, o compilador infere diretamente o resultado:
str := "hello" + "," + "world" + "!"
_ = strAo gerar o código assembly, podemos ver o resultado. Parte dele é mostrada abaixo:
LEAQ go:string."hello,world!"(SB), AX
MOVQ AX, main.str(SP)Obviamente, o compilador trata isso como uma string completa, cujo valor é determinado em tempo de compilação, não sendo concatenada por runtime.concatstrings em tempo de execução. Apenas a concatenação de variáveis de string é completada em tempo de execução. A assinatura da função é:
func concatstrings(buf *tmpBuf, a []string) stringQuando as variáveis de string a serem concatenadas são menores que 5, as seguintes funções são usadas como substitutas (suposição pessoal: devido à passagem de parâmetros e variáveis anônimas, que são armazenadas na pilha, são melhores para GC do que slices criados em tempo de execução?), embora ainda usem concatstrings para completar a concatenação:
func concatstring2(buf *tmpBuf, a0, a1 string) string {
return concatstrings(buf, []string{a0, a1})
}
func concatstring3(buf *tmpBuf, a0, a1, a2 string) string {
return concatstrings(buf, []string{a0, a1, a2})
}
func concatstring4(buf *tmpBuf, a0, a1, a2, a3 string) string {
return concatstrings(buf, []string{a0, a1, a2, a3})
}
func concatstring5(buf *tmpBuf, a0, a1, a2, a3, a4 string) string {
return concatstrings(buf, []string{a0, a1, a2, a3, a4})
}Abaixo veremos o que a função concatstrings faz:
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
// Pula se o comprimento for 0
if n == 0 {
continue
}
// Overflow numérico
if l+n < l {
throw("string concatenation too long")
}
l += n
// Contagem
count++
idx = i
}
// Retorna string vazia se não houver strings
if count == 0 {
return ""
}
// Retorna diretamente se houver apenas uma string
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
// Aloca memória para a nova string
s, b := rawstringtmp(buf, l)
for _, x := range a {
// Copia
copy(b, x)
// Trunca
b = b[len(x):]
}
return s
}Primeiro, estatísticas do comprimento total e quantidade de strings a serem concatenadas são calculadas. Em seguida, a memória é alocada com base no comprimento total. A função rawstringtmp retorna uma string s e um slice de bytes b. Embora seu comprimento seja determinado, eles não têm conteúdo, pois são essencialmente dois ponteiros para novos endereços de memória. O código de alocação de memória é:
func rawstring(size int) (s string, b []byte) {
// Sem tipo especificado
p := mallocgc(uintptr(size), nil, false)
// Embora a memória tenha sido alocada, não há nada nela
return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}A string s retornada é para conveniência de representação, e o slice de bytes b é para facilitar a modificação da string. Ambos apontam para o mesmo endereço de memória.
for _, x := range a {
// Copia
copy(b, x)
// Trunca
b = b[len(x):]
}A função copy chama runtime.slicecopy em tempo de execução, que copia diretamente a memória de src para o endereço dst. Após copiar todas as strings, o processo de concatenação é concluído. Se as strings copiadas forem muito grandes, esse processo consumirá considerável desempenho.
Conversão
Como mencionado anteriormente, strings não podem ser modificadas. Se tentar modificar, nem mesmo a compilação será bem-sucedida. Go reportará o seguinte erro:
str := "hello" + "," + "world" + "!"
str[0] = '1'cannot assign to string (neither addressable nor a map index expression)Para modificar uma string, é necessário primeiro convertê-la para slice de bytes []byte. O uso é muito simples:
bs := []byte(str)Internamente chama a função runtime.stringtoslicebyte, cuja lógica é muito simples:
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
b = rawbyteslice(len(s))
}
copy(b, s)
return b
}Se o comprimento da string for menor que o comprimento do buffer, retorna diretamente o slice de bytes do buffer. Isso pode economizar memória na conversão de strings pequenas. Caso contrário, aloca uma área de memória equivalente ao comprimento da string e copia a string para o novo endereço de memória. A função rawbyteslice(len(s)) faz o mesmo que a função rawstring anterior, ambas alocam memória.
Da mesma forma, slices de bytes podem ser facilmente convertidos para strings na sintaxe:
str := string([]byte{'h','e','l','l','o'})Internamente chama a função runtime.slicebytetostring, também fácil de entender:
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) string {
if n == 0 {
return ""
}
if n == 1 {
p := unsafe.Pointer(&staticuint64s[*ptr])
if goarch.BigEndian {
p = add(p, 7)
}
return unsafe.String((*byte)(p), 1)
}
var p unsafe.Pointer
if buf != nil && n <= len(buf) {
p = unsafe.Pointer(buf)
} else {
p = mallocgc(uintptr(n), nil, false)
}
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return unsafe.String((*byte)(p), n)
}Primeiro trata os casos especiais de slice com comprimento 0 e 1, onde não é necessário copiar memória. Em seguida, usa a memória do buffer se o comprimento for menor que o buffer, caso contrário aloca nova memória. Finalmente, copia a memória diretamente usando a função memmove. A memória copiada não tem nenhuma relação com a memória de origem, então pode ser modificada livremente.
Vale notar que ambos os métodos de conversão acima requerem cópia de memória. Se a memória a ser copiada for muito grande, o consumo de desempenho também será grande. Na atualização para go1.20, o pacote unsafe atualizou as seguintes funções:
// Passa o ponteiro de tipo do endereço de memória e o comprimento dos dados, retorna sua forma de slice
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
// Passa um slice, obtém o ponteiro para seu array subjacente
func SliceData(slice []ArbitraryType) *ArbitraryType
// De acordo com o endereço e comprimento passados, retorna a string
func String(ptr *byte, len IntegerType) string
// Passa uma string, retorna seu endereço de memória inicial, mas os bytes retornados não podem ser modificados
func StringData(str string) *byteEspecialmente as funções String e StringData, elas não envolvem cópia de memória e também podem completar a conversão. No entanto, é necessário garantir que os dados sejam somente leitura e não haverá modificações posteriores. Caso contrário, a string mudará. Veja o exemplo abaixo:
func main() {
bs := []byte("hello,world!")
s := unsafe.String((*byte)(unsafe.SliceData(bs)), len(bs))
bs[0] = 'b'
fmt.Println(s)
}Primeiro obtém o endereço do array subjacente do slice de bytes através de SliceData, depois obtém sua forma de string através de String. Em seguida, modifica diretamente o slice de bytes, e a string também mudará, o que viola a intenção original das strings. Vejamos outro exemplo:
func main() {
str := "hello,world!"
bytes := unsafe.Slice(unsafe.StringData(str), len(str))
fmt.Println(bytes)
// fatal
bytes[0] = 'b'
fmt.Println(str)
}Após obter a forma de slice da string, se tentar modificar o slice de bytes, ocorrerá diretamente fatal. Vamos trocar a forma de declarar a string para ver a diferença:
func main() {
var str string
fmt.Scanln(&str)
bytes := unsafe.Slice(unsafe.StringData(str), len(str))
fmt.Println(bytes)
bytes[0] = 'b'
fmt.Println(str)
}hello,world!
[104 101 108 108 111 44 119 111 114 108 100 33]
bello,world!Pelo resultado podemos ver que a modificação foi bem-sucedida. O motivo do fatal anterior é que a variável str armazena um literal de string. Literais de string são armazenados em segmento de dados somente leitura, não na pilha ou heap, o que fundamentalmente impede a possibilidade de modificação de strings declaradas com literais. Para uma variável de string comum, essencialmente ela pode ser modificada, mas o compilador não permite essa escrita. Em resumo, usar funções unsafe para operar conversão de strings não é seguro, a menos que possa garantir que nunca modificará os dados.
Iteração
s := "hello world!"
for i, r := range s {
fmt.Println(i, r)
}Para lidar com caracteres de múltiplos bytes, geralmente se usa o loop for range para iterar strings. Quando se usa for range para iterar uma string, o compilador expande durante a compilação para código na seguinte forma:
ha := s
for hv1 := 0; hv1 < len(ha); {
hv1t := hv1
hv2 := rune(ha[hv1])
// Julga se é um caractere de byte único
if hv2 < utf8.RuneSelf {
hv1++
} else {
hv2, hv1 = decoderune(ha, hv1)
}
i, r = hv1t, hv2
// Corpo do loop
}No código expandido, o loop for range é substituído por um loop for clássico. No loop, julga se o byte atual é um caractere de byte único. Se for um caractere de múltiplos bytes, chama a função em tempo de execução runtime.decoderune para obter sua codificação completa. Em seguida, atribui a i e r. Após o processamento, chega à execução do corpo do loop definido no código fonte.
O trabalho de construir o código intermediário é completado pela função walkRange em cmd/compile/internal/walk/range.go, que também é responsável por processar todos os tipos que podem ser iterados por for range. Não será expandido aqui. Se estiver interessado, pode pesquisar por conta própria.
