What a Go string really is
In Go, a string is immutable(cannot be changed once created). Internally, it’s just :
type string struct {
Data *byte // pointer to the underlying byte array
Len int // length of the string in bytes
}
Actually the real definition in runtime package is as below, you can refer to link
type stringStruct struct {
str unsafe.Pointer
len int
}
So a string is basically a read-only slice of bytes - but not exactly a slice. It doesn’t have capacity, and you can’t modify its contents.
Encoding
Go strings are stored as UTF-8 encoded bytes by default. That means:
- Each string is a sequence of bytes
- A character(rune) might take 1-4 bytes depending on Unicode
s := "Goalng😊"
fmt.Println(len(s)) // 10
fmt.Println(utf8.RuneCountInString(s)) // 7
So len(s) counts bytes, not characters.
Indexing and slicing
Indexing a string gives you the raw byte, not a character.
s := "Golang😊"
fmt.Printf("%T\n", s[0]) // uint8
fmt.Println(s[0]) // 71 (ASCII code for 'G')
So there are two ways to iterate the string
- we get the raw byte
s := "Golang😊"
for i:=0; i < len(s); i++ {
fmt.Println(s[i])
}
// 71 111 108 97 110 103 240 159 152 138
- we get the character
s := "Golang😊"
for _, c := range s {
fmt.Println(c)
}
// G o l a n g 😊
Immutability
Strings can’t be changed in-place, if you do this
s := "Hello"
s[0] = 'W' // complie error
we must create a new one
s2 := "W" + s[1:]
Relation to []byte and []rune
- []byte -> raw underlying bytes
- []rune -> Unicode code points
s := "😊"
fmt.Println([]byte(s)) // [240 159 152 138]
fmt.Println([]rune(s)) // [128522]
memory diagram
s (string value) ──► Data ──► [71 240 159 152 138]
Len = 5
if we convert to []byte
b := []byte(s)
b (slice) ──► Data ──► [71 240 159 152 138]
Len = 5
Cap = 5
if we convert to []rune
r := []rune(s)
r (slice of runes) ──► Data ──► [71 128522]
Len = 2
Cap = 2
Inspecting a Go string
We can user the reflect.StringHeader struct (which mirrors the internal runtime stringStruct) + unsafe to look at
the memory layout of a string.
package main
import (
"fmt"
"unsafe"
"reflect"
)
func main() {
s := "Golang😊"
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("Data Pointer: %x \n", hdr.Data) //address of first byte
fmt.Printf("Length: %d \n", hdr.Len) // 10
}
The output might vary by machine