String dan rune

Representasi UTF-8 serta perbedaan iterasi per byte dan per rune.

Daftar isi

Topik ini membahas cara Go menyimpan teks, yaitu string sebagai deretan byte yang dienkode UTF-8, beserta perbedaan iterasi per byte dan per rune yang muncul dari representasi itu. Perbedaannya bukan sekadar teori, karena panjang string, pengindeksan, dan perulangan menghasilkan angka yang berbeda tergantung sudut pandangnya. Setelah membaca topik ini, pilihan antara iterasi byte dan iterasi rune dapat dilakukan dengan alasan yang tepat, dan sumber potensi kesalahan saat menangani teks non-ASCII menjadi jelas.

String adalah deretan byte#

Tipe string di Go menyimpan deretan byte yang tidak dapat diubah (immutable), dan fungsi len mengembalikan jumlah byte, bukan jumlah karakter. Encoding yang dipakai adalah UTF-8, yaitu skema penkodean Unicode yang memakai 1 byte untuk karakter ASCII klasik dan 2 hingga 4 byte untuk karakter di luar itu. Akibatnya, sebuah string yang terlihat pendek di layar dapat berisi lebih banyak byte daripada yang diperkirakan. Program berikut membedah string 24°C, yaitu teks suhu biasa dengan simbol derajat:

go iterasi-byte.go
package main

import "fmt"

func main() {
	suhu := "24°C"

	fmt.Println("len(suhu) =", len(suhu), "byte")
	for i := 0; i < len(suhu); i++ {
		fmt.Printf("indeks %d berisi byte %d\n", i, suhu[i])
	}
}
text Keluaran
len(suhu) = 5 byte
indeks 0 berisi byte 50
indeks 1 berisi byte 52
indeks 2 berisi byte 194
indeks 3 berisi byte 176
indeks 4 berisi byte 67

Keluarannya memperlihatkan anatomi UTF-8 dengan jelas. Karakter 2, 4, dan C masing-masing satu byte, yaitu 50, 52, dan 67 sesuai tabel ASCII. Simbol ° tidak terdapat di ASCII, sehingga UTF-8 mengenkodenya menjadi dua byte, yaitu 194 dan 176. Totalnya 5 byte, dan itulah nilai yang dilaporkan len. Pengindeksan seperti suhu[i] juga selalu menghasilkan satu byte, sehingga pembacaan per indeks adalah pembacaan byte, bukan pembacaan karakter.

Rune sebagai karakter Unicode#

Rune adalah nama Go untuk satu nilai Unicode, yaitu code point atau titik kode, dan tipenya adalah alias dari int32. Nilainya ditulis dengan tanda kutip tunggal, misalnya 'A' berisi angka 65, sehingga rune dapat diperlakukan sebagai bilangan bila diperlukan. Penting dicatat bahwa rune belum tentu sama dengan karakter yang digambar di layar, karena beberapa karakter visual tersusun dari lebih dari satu titik kode. Untuk teks pada umumnya, pemetaan satu karakter visual ke satu rune sudah cukup sebagai model kerja.

Konversi antara string, byte, dan rune bersifat eksplisit dan selalu menyalin data. Ekspresi []byte(s) menghasilkan salinan byte string, sedangkan []rune(s) mengurai UTF-8 lalu menghasilkan potongan rune. Pada 24°C, konversi rune menghasilkan empat nilai, yaitu [50 52 176 67], sedangkan konversi byte menghasilkan lima nilai, yaitu [50 52 194 176 67]. Angka 194 dan 176 itulah isi byte simbol derajat yang sama seperti pada bagian sebelumnya.

Dari sudut pandang ini, len dan jumlah karakter menjawab pertanyaan yang berbeda. Jumlah rune pada 24°C adalah 4, sedangkan len melaporkan 5 byte. Menghitung jumlah rune dilakukan dengan iterasi range atau dengan fungsi utf8.RuneCountInString dari package unicode/utf8.

Iterasi per rune dengan for range#

Bentuk for i, r := range s mengiterasi string per rune, bukan per byte. Variabel i tetap berupa indeks byte tempat rune itu dimulai, sedangkan r berisi nilainya. Program berikut mengulangi pembacaan 24°C dari sudut pandang rune:

go iterasi-rune.go
package main

import "fmt"

func main() {
	suhu := "24°C"

	for i, r := range suhu {
		fmt.Printf("rune %q berada pada indeks byte %d\n", r, i)
	}
}
text Keluaran
rune '2' berada pada indeks byte 0
rune '4' berada pada indeks byte 1
rune '°' berada pada indeks byte 2
rune 'C' berada pada indeks byte 4

Loncatan indeks dari 2 ke 4 adalah bukti paling jelas dari penkodean UTF-8. Rune '°' dimulai pada indeks byte 2 dan memakan dua byte, sehingga rune berikutnya berada pada indeks byte 4. Indeks yang dihasilkan range tetap sah untuk pemotongan string, karena nilainya menunjuk batas rune yang benar.

Memilih sudut pandang byte atau rune#

Kedua sudut pandang memanggil peran yang berbeda, dan tabel berikut merangkumnya.

AspekIterasi per byteIterasi per rune
Bentuk kodefor i := 0; i < len(s); i++for i, r := range s
Nilai tiap langkahSatu byte (uint8)Satu rune (alias int32)
Jumlah langkahlen(s), yaitu jumlah byteJumlah titik kode
Pemakaian yang tepatPemrosesan biner, penghitungan byteTeks untuk dibaca manusia

Praktik amannya adalah menulis perulangan indeks klasik hanya untuk urusan byte, misalnya menghitung ukuran atau memilah data biner, dan memakai range untuk teks yang dibaca manusia. Pemotongan s[:i] pun aman selama i berasal dari indeks range, karena selalu berada di batas rune. Sebaliknya, memotong pada indeks byte tengah rune menghasilkan string yang rusak, yaitu deretan byte yang tidak lagi merupakan UTF-8 sah. Pada keluaran, kerusakan itu tampak sebagai tanda tanya atau kotak.

Jebakan serupa muncul pada pembalikan teks dengan menyalin byte dari belakang ke depan. Apabila teks memuat rune berkode lebih dari satu byte, urutan byte penyusun rune ikut terbalik dan hasilnya adalah teks rusak. Pembalikan yang benar bekerja pada rune, yaitu menyalin rune satu per satu ke potongan baru, sehingga batas rune tetap terjaga pada hasil akhirnya.

Ringkasan#

  • string adalah deretan byte UTF-8 yang tidak dapat diubah, dan len melaporkan jumlah byte, bukan jumlah karakter.
  • Pengindeksan s[i] menghasilkan satu byte, sedangkan for range menghasilkan pasangan indeks byte dan rune.
  • Rune adalah alias int32 untuk satu titik kode Unicode, dan karakter di luar ASCII memakai lebih dari satu byte.
  • Iterasi per byte untuk urusan biner dan iterasi per rune untuk teks manusiawi, karena keduanya menjawab pertanyaan yang berbeda.

Referensi#