Skip to content

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.

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:

go
// 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 string

Desta descrição podemos obter as seguintes informações:

  • string é uma coleção de bytes de 8 bits
  • O tipo string é normalmente codificado em UTF-8
  • string pode ser vazia, mas nunca nil
  • string é 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).

go
// 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 string
  • Len, número de bytes da string

Abaixo está um exemplo de acesso ao endereço da string através de ponteiro unsafe:

go
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:

go
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 +:

go
var (
    hello = "hello"
    dot   = ","
    world = "world"
    last  = "!"
)
str := hello + dot + world + last

A 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:

go
str := "hello" + "," + "world" + "!"
_ = str

Ao 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 é:

go
func concatstrings(buf *tmpBuf, a []string) string

Quando 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:

go
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:

go
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 é:

go
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.

go
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:

go
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:

go
bs := []byte(str)

Internamente chama a função runtime.stringtoslicebyte, cuja lógica é muito simples:

go
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:

go
str := string([]byte{'h','e','l','l','o'})

Internamente chama a função runtime.slicebytetostring, também fácil de entender:

go
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:

go
// 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) *byte

Especialmente 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:

go
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:

go
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:

go
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

go
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:

go
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.

Golang por www.golangdev.cn edit