Skip to content

string

string adalah tipe data dasar yang sangat umum di Go, dan merupakan tipe data pertama yang saya temui dalam bahasa Go

go
package main

import "fmt"

func main() {
  fmt.Println("hello,world!")
}

Saya yakin kebanyakan orang pernah mengetik kode ini saat pertama kali mengenal Go. Di builtin/builtin.go ada deskripsi singkat tentang string

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

Dari pernyataan di atas dapat diperoleh beberapa informasi berikut

  • string adalah kumpulan byte 8-bit

  • Tipe string biasanya di-encode dengan UTF-8

  • string bisa kosong, tetapi tidak bisa nil

  • string bersifat immutable (tidak dapat diubah)

Beberapa karakteristik ini seharusnya sudah sangat familiar bagi mereka yang sering menggunakan Go, jadi mari lihat sesuatu yang berbeda di bawah ini.

Struktur

Di Go, string direpresentasikan oleh struktur runtime.stringStruct pada runtime, tetapi tidak diekspos ke publik, sebagai gantinya dapat menggunakan reflect.StringHeader.

TIP

Meskipun StringHeader sudah di-deprecated sejak versi go1.21, namun sangat intuitif, konten di bawah ini masih akan menggunakannya untuk penjelasan, tidak mempengaruhi pemahaman, lihat detailnya di 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
}

Penjelasan field di antaranya adalah sebagai berikut

  • Data, adalah pointer yang menunjuk ke alamat awal memori string
  • Len, jumlah byte string

Berikut adalah contoh mengakses alamat string melalui pointer 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]))))))
  }
}

Namun Go sekarang merekomendasikan penggunaan unsafe.StringData sebagai pengganti

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]))))))
  }
}

Kedua outputnya sama

h e l l o , w o r l d !

String pada dasarnya adalah blok memori yang berkelanjutan, setiap alamat menyimpan satu byte, dengan kata lain adalah array byte, hasil yang diperoleh melalui fungsi len adalah jumlah byte, bukan jumlah karakter dalam string, terutama ketika karakter dalam string adalah karakter non-ASCII.

string sendiri hanya menempati memori yang sangat kecil yaitu pointer yang menunjuk ke data sebenarnya, dengan demikian biaya pengiriman string akan sangat rendah. Menurut pendapat pribadi, karena hanya memegang referensi memori, jika dapat dimodifikasi secara sembarangan, akan sulit mengetahui di kemudian hari apakah data yang ditunjuk masih data yang diinginkan (baik menggunakan refleksi atau paket unsafe), kecuali jika pengguna data lama tidak akan pernah membutuhkan string tersebut setelah digunakan, keunggulan lainnya adalah secara inheren aman secara konkuren, tidak ada yang dapat memodifikasinya dalam kondisi normal.

Penggabungan

Sintaks penggabungan string adalah sebagai berikut, langsung menggunakan operator + untuk penggabungan.

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

Operasi penggabungan diselesaikan oleh fungsi runtime.concatstrings pada runtime, jika seperti berikut ini penggabungan literal, compiler akan langsung menyimpulkan hasilnya.

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

Dengan menampilkan kode assembly-nya dapat diketahui hasilnya, sebagian adalah sebagai berikut

LEAQ    go:string."hello,world!"(SB), AX
MOVQ    AX, main.str(SP)

Jelas compiler langsung menganggapnya sebagai string lengkap, nilainya sudah ditentukan pada saat kompilasi, tidak akan digabungkan oleh runtime.concatstrings pada runtime, hanya penggabungan variabel string yang akan diselesaikan pada runtime, signature fungsinya adalah sebagai berikut, ia menerima array byte dan slice string.

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

Ketika jumlah variabel string yang digabungkan kurang dari 5, akan menggunakan fungsi di bawah ini sebagai pengganti (dugaan pribadi: karena parameter dan variabel anonim, keduanya disimpan di stack, lebih baik untuk GC dibandingkan slice yang dibuat pada runtime?), meskipun pada akhirnya masih diselesaikan oleh concatstrings.

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})
}

Mari lihat apa yang dilakukan fungsi concatstrings di dalamnya

go
func concatstrings(buf *tmpBuf, a []string) string {
  idx := 0
  l := 0
  count := 0
  for i, x := range a {
    n := len(x)
    // Lewati jika panjang 0
    if n == 0 {
      continue
    }
    // Overflow perhitungan numerik
    if l+n < l {
      throw("string concatenation too long")
    }
    l += n
    // Hitung jumlah
    count++
    idx = i
  }
  // Kembalikan string kosong jika tidak ada string
  if count == 0 {
    return ""
  }

  // Langsung kembalikan jika hanya ada satu string
  if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
    return a[idx]
  }
  // Alokasikan memori untuk string baru
  s, b := rawstringtmp(buf, l)
  for _, x := range a {
        // Salin
    copy(b, x)
        // Potong
    b = b[len(x):]
  }
  return s
}

Hal pertama yang dilakukan adalah menghitung total panjang dan jumlah string yang akan digabungkan, kemudian mengalokasikan memori berdasarkan total panjang, fungsi rawstringtmp akan mengembalikan string s dan slice byte b, meskipun panjangnya ditentukan tetapi tidak memiliki konten apa pun, karena pada dasarnya keduanya adalah pointer yang menunjuk ke alamat memori baru, kode alokasi memori adalah sebagai berikut

go
func rawstring(size int) (s string, b []byte) {
    // Tidak menentukan tipe
  p := mallocgc(uintptr(size), nil, false)
    // Meskipun memori dialokasikan tetapi tidak ada apa-apa di atasnya
  return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}

String s yang dikembalikan adalah untuk kemudahan representasi, slice byte b adalah untuk kemudahan memodifikasi string, keduanya menunjuk ke alamat memori yang sama.

go
for _, x := range a {
    // Salin
    copy(b, x)
    // Potong
    b = b[len(x):]
}

Fungsi copy memanggil runtime.slicecopy pada runtime, pekerjaan yang dilakukannya adalah langsung menyalin memori src ke alamat dst, setelah semua string disalin, seluruh proses penggabungan selesai. Jika string yang disalin sangat besar, proses ini akan sangat memakan performa.

Konversi

Sebelumnya disebutkan bahwa string sendiri tidak dapat dimodifikasi, jika mencoba memodifikasi bahkan kompilasi tidak akan lolos, Go akan melaporkan error sebagai berikut

go
str := "hello" + "," + "world" + "!"
str[0] = '1'
cannot assign to string (neither addressable nor a map index expression)

Jika ingin memodifikasi string, perlu mengonversi tipenya ke slice byte []byte terlebih dahulu, penggunaannya sangat mudah

go
bs := []byte(str)

Di internal memanggil fungsi runtime.stringtoslicebyte, logikanya masih sangat sederhana, kodenya adalah sebagai berikut

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
}

Jika panjang string kurang dari panjang buffer langsung kembalikan slice byte buffer,这样可以节省小字符串转换时的内存。否则,就会开辟一片与字符串长度相当的内存,然后将字符串复制到新的内存地址中,其中函数 rawbyteslice(len(s)) 所做的事与之前 rawstring 函数类似,都是分配内存。

Demikian juga, slice byte secara sintaksis juga dapat dengan mudah dikonversi menjadi string

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

Di internal memanggil fungsi runtime.slicebytetostring, juga mudah dipahami, kodenya adalah sebagai berikut

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)
}

Pertama menangani kasus khusus panjang slice 0 dan 1, dalam kasus ini tidak perlu menyalin memori. Kemudian jika kurang dari panjang buffer gunakan memori buffer, jika tidak alokasikan memori baru, terakhir gunakan fungsi memmove untuk langsung menyalin memori, memori yang disalin tidak ada hubungan dengan memori sumber, jadi dapat dimodifikasi secara bebas.

Yang perlu diperhatikan adalah, kedua metode konversi di atas memerlukan penyalinan memori, jika memori yang akan disalin sangat besar, konsumsi performa juga akan besar. Saat versi diperbarui ke go1.20, paket unsafe memperbarui beberapa fungsi berikut.

go
// Masukkan pointer tipe ke alamat memori dan panjang data, kembalikan bentuk slice-nya
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

// Masukkan sebuah slice, dapatkan pointer ke array tingkat rendahnya
func SliceData(slice []ArbitraryType) *ArbitraryType

// Kembalikan string berdasarkan alamat dan panjang yang dimasukkan
func String(ptr *byte, len IntegerType) string

// Masukkan sebuah string, kembalikan alamat memori awalnya, tetapi byte yang dikembalikan tidak dapat dimodifikasi
func StringData(str string) *byte

Terutama fungsi String dan StringData, keduanya tidak melibatkan penyalinan memori, juga dapat menyelesaikan konversi, namun perlu diperhatikan bahwa prasyarat menggunakannya adalah memastikan data hanya baca, tidak ada modifikasi di kemudian hari, jika tidak string akan berubah, lihat contoh berikut.

go
func main() {
  bs := []byte("hello,world!")
  s := unsafe.String((*byte)(unsafe.SliceData(bs)), len(bs))
  bs[0] = 'b'
  fmt.Println(s)
}

Pertama melalui SliceData mendapatkan alamat array tingkat rendah slice byte, kemudian melalui String mendapatkan bentuk ekspresi string-nya, kemudian langsung memodifikasi slice byte, string juga akan berubah, ini jelas bertentangan dengan tujuan awal string. Mari lihat contoh lain

go
func main() {
  str := "hello,world!"
  bytes := unsafe.Slice(unsafe.StringData(str), len(str))
  fmt.Println(bytes)
    // fatal
  bytes[0] = 'b'
  fmt.Println(str)
}

Setelah mendapatkan bentuk ekspresi slice string, jika mencoba memodifikasi slice byte, akan langsung fatal, mari ganti cara deklarasi string untuk melihat apa bedanya.

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!

Dari hasil dapat dilihat, memang berhasil dimodifikasi. Alasan fatal sebelumnya adalah karena variabel str menyimpan literal string, literal string disimpan di segmen data hanya baca, bukan di heap stack, pada dasarnya mengakhiri kemungkinan literal string yang dideklarasikan akan dimodifikasi di kemudian hari, untuk variabel string biasa, pada dasarnya memang dapat dimodifikasi, tetapi cara penulisan ini tidak diizinkan oleh compiler. Singkatnya, menggunakan fungsi unsafe untuk mengoperasikan konversi string tidak aman, kecuali dapat memastikan tidak akan pernah memodifikasi data.

Traversing

go
s := "hello world!"
for i, r := range s {
  fmt.Println(i, r)
}

Untuk menangani kasus multi-byte character, traversing string biasanya menggunakan loop for range. Saat menggunakan for range untuk traversing string, compiler akan展开 menjadi kode dalam bentuk berikut selama kompilasi

go
ha := s
for hv1 := 0; hv1 < len(ha); {
    hv1t := hv1
    hv2 := rune(ha[hv1])
    // Apakah karakter single byte
    if hv2 < utf8.RuneSelf {
        hv1++
    } else {
        hv2, hv1 = decoderune(ha, hv1)
    }
    i, r = hv1t, hv2
  // Loop body
}

Dalam kode yang展开,loop for range akan diganti dengan loop for klasik, dalam loop akan判断 apakah byte saat ini adalah karakter single byte, jika karakter multi byte akan memanggil fungsi runtime runtime.decoderune untuk mendapatkan encoding lengkapnya, kemudian assign ke i,r, setelah penanganan selesai sampai ke eksekusi loop body yang didefinisikan dalam kode sumber.

Pekerjaan membangun kode perantara diselesaikan oleh fungsi walkRange di cmd/compile/internal/walk/range.go, sekaligus bertanggung jawab untuk menangani semua tipe yang dapat di-traversing oleh for range, di sini tidak akan展开,jika tertarik dapat memahaminya sendiri.

Golang by www.golangdev.cn edit