string
string è un tipo di dato di base molto comune in Go, ed è stato il primo tipo di dato che ho incontrato quando ho iniziato a imparare Go.
package main
import "fmt"
func main() {
fmt.Println("hello,world!")
}Credo che la maggior parte delle persone abbia scritto questo codice quando ha iniziato a imparare Go. In builtin/builtin.go c'è una breve descrizione di string:
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string stringDa questa descrizione si possono ottenere le seguenti informazioni:
stringè una collezione di byte a 8 bit- Il tipo
stringè solitamente codificato inUTF-8 stringpuò essere vuoto, ma nonnilstringè immutabile
Queste caratteristiche dovrebbero essere ben note a chi usa Go da tempo. Ora vediamo qualcosa di diverso.
Struttura
In Go, le stringhe sono rappresentate a runtime dalla struttura runtime.stringStruct, che non è esposta pubblicamente. In alternativa, si può usare reflect.StringHeader.
TIP
Sebbene StringHeader sia stato deprecato nella versione go1.21, è comunque molto intuitivo e lo userò per la spiegazione seguente. Questo non influisce sulla comprensione; per maggiori dettagli vedere 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
}I campi sono spiegati come segue:
Data: un puntatore all'indirizzo di inizio della memoria della stringaLen: il numero di byte della stringa
Di seguito è un esempio di accesso all'indirizzo di una stringa tramite puntatore 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]))))))
}
}Tuttavia, Go ora raccomanda di usare unsafe.StringData come sostituto:
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]))))))
}
}Entrambi producono lo stesso output:
h e l l o , w o r l d !
Una stringa è essenzialmente un'area di memoria continua, dove ogni indirizzo memorizza un byte. In altre parole, è un array di byte. Il risultato della funzione len è il numero di byte, non il numero di caratteri nella stringa. Questo è particolarmente vero quando la stringa contiene caratteri non ASCII.
string stesso occupa poca memoria: solo un puntatore ai dati reali. Questo rende il passaggio delle stringhe molto efficiente. Personalmente, poiché detiene solo un riferimento alla memoria, se potesse essere modificato liberamente, sarebbe difficile sapere in seguito se i dati originali puntati sono ancora quelli desiderati (a meno di usare reflection o il pacchetto unsafe). Un altro vantaggio è che è intrinsecamente thread-safe: nessuno può modificarlo in condizioni normali.
Concatenazione

La sintassi per la concatenazione di stringhe è la seguente, usando direttamente l'operatore +:
var (
hello = "hello"
dot = ","
world = "world"
last = "!"
)
str := hello + dot + world + lastL'operazione di concatenazione viene completata a runtime dalla funzione runtime.concatstrings. Se si tratta di una concatenazione di letterali come quella seguente, il compilatore dedurrà direttamente il risultato:
str := "hello" + "," + "world" + "!"
_ = strOutput del codice assembly per sapere il risultato, parte del quale è mostrato di seguito:
LEAQ go:string."hello,world!"(SB), AX
MOVQ AX, main.str(SP)È evidente che il compilatore lo tratta come una stringa completa, il cui valore è determinato in fase di compilazione e non viene concatenato a runtime da runtime.concatstrings. Solo la concatenazione di variabili stringa viene completata a runtime. La firma della funzione è la seguente e riceve un array di byte e una slice di stringhe:
func concatstrings(buf *tmpBuf, a []string) stringQuando il numero di stringhe da concatenare è inferiore a 5, verranno usate le seguenti funzioni al posto di quella sopra (personalmente ipotizzo: poiché i parametri e le variabili anonime sono passati sullo stack, sono migliori per il GC rispetto alle slice create a runtime?), anche se alla fine la concatenazione viene comunque completata da concatstrings:
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})
}Vediamo cosa fa la funzione concatstrings:
func concatstrings(buf *tmpBuf, a []string) string {
idx := 0
l := 0
count := 0
for i, x := range a {
n := len(x)
// Salta se la lunghezza è 0
if n == 0 {
continue
}
// Overflow nel calcolo numerico
if l+n < l {
throw("string concatenation too long")
}
l += n
// Conta
count++
idx = i
}
// Nessuna stringa, ritorna stringa vuota
if count == 0 {
return ""
}
// Se c'è solo una stringa, ritorna direttamente
if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
return a[idx]
}
// Alloca memoria per la nuova stringa
s, b := rawstringtmp(buf, l)
for _, x := range a {
// Copia
copy(b, x)
// Tronca
b = b[len(x):]
}
return s
}La prima cosa che fa è calcolare la lunghezza totale e il numero di stringhe da concatenare, quindi alloca memoria in base alla lunghezza totale. La funzione rawstringtmp restituisce una stringa s e una slice di byte b. Sebbene la loro lunghezza sia determinata, non hanno alcun contenuto perché sono essenzialmente due puntatori a nuovi indirizzi di memoria. Il codice per l'allocazione della memoria è il seguente:
func rawstring(size int) (s string, b []byte) {
// Nessun tipo specificato
p := mallocgc(uintptr(size), nil, false)
// Anche se la memoria è allocata, non c'è nulla
return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}La stringa s restituita è per comodità di rappresentazione, mentre la slice di byte b è per comodità di modifica della stringa. Entrambe puntano allo stesso indirizzo di memoria.
for _, x := range a {
// Copia
copy(b, x)
// Tronca
b = b[len(x):]
}La funzione copy chiama runtime.slicecopy a runtime, il cui lavoro è copiare direttamente la memoria da src all'indirizzo dst. Una volta copiati tutti i byte, il processo di concatenazione è completato. Se le stringhe da copiare sono molto grandi, questo processo consumerà molte prestazioni.
Conversione
Come menzionato in precedenza, le stringhe non possono essere modificate. Se si tenta di modificarle, non si può nemmeno compilare. Go riporterà il seguente errore:
str := "hello" + "," + "world" + "!"
str[0] = '1'cannot assign to string (neither addressable nor a map index expression)Per modificare una stringa, è necessario prima convertirla in una slice di byte []byte. È molto semplice da usare:
bs := []byte(str)Internamente chiama la funzione runtime.stringtoslicebyte, la cui logica è molto semplice:
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 la lunghezza della stringa è inferiore alla lunghezza del buffer, restituisce direttamente la slice di byte del buffer. Questo può risparmiare memoria per le conversioni di piccole stringhe. Altrimenti, alloca un'area di memoria della stessa lunghezza della stringa e copia la stringa nel nuovo indirizzo di memoria. La funzione rawbyteslice(len(s)) fa la stessa cosa della funzione rawstring precedente, ovvero alloca memoria.
Allo stesso modo, le slice di byte possono essere facilmente convertite in stringhe:
str := string([]byte{'h','e','l','l','o'})Internamente chiama la funzione runtime.slicebytetostring, anche questa facile da capire:
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)
}Per prima cosa gestisce i casi speciali in cui la slice ha lunghezza 0 o 1, in cui non è necessaria la copia della memoria. Poi, se la lunghezza è inferiore alla lunghezza del buffer, usa la memoria del buffer; altrimenti alloca nuova memoria. Infine, usa la funzione memmove per copiare direttamente la memoria. La memoria copiata non ha alcuna relazione con la memoria originale, quindi può essere modificata liberamente.
Vale la pena notare che entrambi i metodi di conversione sopra menzionati richiedono la copia della memoria. Se la memoria da copiare è molto grande, il consumo di prestazioni sarà elevato. Con l'aggiornamento alla versione go1.20, il pacchetto unsafe ha introdotto le seguenti funzioni:
// Passa un puntatore di tipo all'indirizzo di memoria e la lunghezza dei dati, restituisce la sua forma di slice
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
// Passa una slice, ottieni un puntatore al suo array sottostante
func SliceData(slice []ArbitraryType) *ArbitraryType
// In base all'indirizzo e alla lunghezza passati, restituisce una stringa
func String(ptr *byte, len IntegerType) string
// Passa una stringa, restituisce il suo indirizzo di memoria iniziale, ma i byte restituiti non possono essere modificati
func StringData(str string) *byteIn particolare le funzioni String e StringData non comportano la copia della memoria e possono anche completare la conversione. Tuttavia, è necessario assicurarsi che i dati siano di sola lettura e non verranno modificati in seguito; altrimenti la stringa cambierà. Vediamo l'esempio seguente:
func main() {
bs := []byte("hello,world!")
s := unsafe.String((*byte)(unsafe.SliceData(bs)), len(bs))
bs[0] = 'b'
fmt.Println(s)
}Per prima cosa ottiene l'indirizzo dell'array sottostante della slice di byte tramite SliceData, poi ottiene la sua forma di stringa tramite String. Successivamente, modificando direttamente la slice di byte, anche la stringa cambierà, il che显然 contraddice lo scopo originale delle stringhe. Vediamo un altro esempio:
func main() {
str := "hello,world!"
bytes := unsafe.Slice(unsafe.StringData(str), len(str))
fmt.Println(bytes)
// fatal
bytes[0] = 'b'
fmt.Println(str)
}Dopo aver ottenuto la forma di slice della stringa, se si tenta di modificare la slice di byte, si verificherà direttamente un fatal. Vediamo ora se c'è qualche differenza cambiando il modo in cui la stringa viene dichiarata:
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!Dal risultato si può vedere che la modifica è effettivamente riuscita. Il motivo per cui prima si verificava un fatal è che la variabile str memorizza un letterale di stringa. I letterali di stringa sono memorizzati in una sezione di dati di sola lettura, non nell'heap o nello stack, il che fondamentalmente elimina la possibilità che le stringhe dichiarate con letterali vengano modificate in seguito. Per una normale variabile stringa, in effetti può essere modificata, ma il compilatore non lo permette. In sintesi, usare le funzioni unsafe per operare la conversione delle stringhe non è sicuro, a meno che non si possa garantire che i dati non verranno mai modificati.
Iterazione
s := "hello world!"
for i, r := range s {
fmt.Println(i, r)
}Per gestire il caso di caratteri multi-byte, per iterare sulle stringhe si usa solitamente il ciclo for range. Quando si usa for range per iterare su una stringa, il compilatore lo espande durante la compilazione nella seguente forma di codice:
ha := s
for hv1 := 0; hv1 < len(ha); {
hv1t := hv1
hv2 := rune(ha[hv1])
// Verifica se è un carattere a byte singolo
if hv2 < utf8.RuneSelf {
hv1++
} else {
hv2, hv1 = decoderune(ha, hv1)
}
i, r = hv1t, hv2
// Corpo del ciclo
}Nel codice espanso, il ciclo for range viene sostituito con un classico ciclo for. Nel ciclo, viene verificato se il byte corrente è un carattere a byte singolo. Se è un carattere multi-byte, viene chiamata la funzione runtime runtime.decoderune per ottenere la sua codifica completa, quindi viene assegnata a i e r. Dopo aver elaborato, si esegue il corpo del ciclo definito nel codice sorgente.
Il lavoro di costruzione del codice intermedio è completato dalla funzione walkRange in cmd/compile/internal/walk/range.go, che si occupa anche di gestire tutti i tipi che possono essere iterati con for range. Non lo espanderò qui; se interessati, potete approfondire autonomamente.
