Aplikasi nyata jarang hanya menampilkan huruf a sampai z: ada “Budi Santoso” yang aman-aman saja, tetapi juga “café”, “日本語”, hingga emoji 🌏. Go menjawabnya dengan satu keputusan desain: string disimpan sebagai byte ber-encoding UTF-8, dan tipe rune merepresentasikan satu karakter Unicode sebagai angka. Pada chapter ini kita akan membedah hubungan byte, rune, dan code point, melihat bagaimana len menghitung byte bukan karakter, lalu menyingkap jebakan normalisasi yang membuat dua teks yang tampak sama ternyata berbeda.
String Menyimpan Byte UTF-8 #
UTF-8 adalah encoding Unicode dengan lebar variabel: satu karakter menempati 1 sampai 4 byte. Seluruh huruf ASCII — alphabet, angka, tanda baca dasar — tetap memakai 1 byte, sehingga teks Inggris dan Indonesia sehari-hari hemat ruang. Karakter lain memakai byte lebih banyak sesuai tabel berikut:
| Teks | len (byte) | Jumlah rune | Karakteristik |
|---|---|---|---|
"Halo" | 4 | 4 | ASCII, 1 byte per karakter |
"café" | 5 | 4 | é menempati 2 byte |
"日本語" | 9 | 3 | Kanji CJK, 3 byte per karakter |
"🌏" | 4 | 1 | Emoji, 4 byte |
Source code Go sendiri diwajibkan ber-encoding UTF-8, sehingga literal "café" yang kita tulis langsung tersimpan sebagai 5 byte. Encoding ini juga kompatibel mundur dengan ASCII.
len Menghitung Byte, Bukan Karakter #
Fungsi builtin len pada string mengembalikan jumlah byte. Untuk menghitung jumlah karakter, package unicode/utf8 menyediakan RuneCountInString yang mendekode string rune demi rune:
fmt.Println(len("日本語"), utf8.RuneCountInString("日本語"))Output yang Dihasilkan:
9 3Angka 9 berasal dari tiga kanji yang masing-masing 3 byte, sedangkan 3 adalah hasil dekode RuneCountInString. Keduanya menjawab pertanyaan yang berbeda: len menjawab “berapa byte yang dibutuhkan di memori”, RuneCountInString menjawab “berapa karakter yang dilihat pembaca”. Untuk teks ASCII murni kedua angka selalu sama; selisihnya baru muncul begitu karakter non-ASCII masuk.
rune Adalah Code Point Bertipe int32 #
Tipe rune adalah alias dari int32 yang nilainya adalah code point Unicode — nomor karakter dalam tabel Unicode. Huruf A bermuatan 65, sedangkan é bermuatan 233. Literal rune ditulis dengan kutip tunggal: 'é'. Loop for range pada string mendekode UTF-8 otomatis dan menghasilkan dua nilai: posisi byte tempat rune dimulai, dan rune itu sendiri:
for i, r := range "café" {
fmt.Println(i, r)
}Output yang Dihasilkan:
0 99
1 97
2 102
3 233Indeks i melompat mengikuti lebar rune: c, a, f masing-masing menempati 1 byte sehingga indeks berjalan 0, 1, 2, lalu rune é dimulai di byte ke-3. Nilai r tercetak sebagai angka karena rune memang int32 — gunakan %q atau string(r) untuk melihat karakternya. Package unicode/utf8 juga menyediakan alat manual: utf8.DecodeRuneInString mengurai rune pertama beserta lebar byte-nya dan utf8.DecodeLastRuneInString mengurai dari belakang.
Normalisasi Unicode #
Satu karakter dapat punya lebih dari satu representasi. Huruf é bisa ditulis sebagai satu rune U+00E9 (bentuk composed), atau sebagai huruf e biasa plus tanda aksen terpisah U+0301 (bentuk decomposed). Kedua string berikut menampilkan hasil yang sama di layar:
fmt.Printf("%q %q\n", "caf\u00E9", "cafe\u0301")Output yang Dihasilkan:
"café" "café"Perbandingan == bekerja pada byte, bukan tampilan, sehingga "caf\u00E9" == "cafe\u0301" bernilai false — panjang bytenya pun berbeda, 5 versus 6. Data dari dua sumber berbeda (input pengguna, basis data) bisa memakai bentuk berbeda. Untuk menyamakannya sebelum dibandingkan, gunakan normalisasi dari package golang.org/x/text/unicode/norm di luar standard library.
Program Utuh #
Berkas utf8-demo.go merangkum seluruh percobaan di atas dalam satu program. Jalankan dari direktori chapter:
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// Teks ASCII: satu karakter menempati tepat satu byte.
fmt.Printf("\"Halo\" : %d byte, %d rune\n", len("Halo"), utf8.RuneCountInString("Halo"))
// Huruf beraksen memakai 2 byte dalam UTF-8.
fmt.Printf("\"café\" : %d byte, %d rune\n", len("café"), utf8.RuneCountInString("café"))
// Kanji memakai 3 byte per karakter.
fmt.Printf("\"日本語\": %d byte, %d rune\n", len("日本語"), utf8.RuneCountInString("日本語"))
// range menghasilkan indeks byte dan rune, bukan nomor karakter.
for i, r := range "日本語" {
fmt.Printf("rune %q mulai di byte %d\n", r, i)
}
// DecodeRuneInString mengurai satu rune dari depan string.
r, lebar := utf8.DecodeRuneInString("日本語")
fmt.Printf("rune pertama %q lebar %d byte\n", r, lebar)
// rune adalah alias int32 yang menyimpan code point.
var kode rune = 'é'
fmt.Printf("rune %q = code point U+%04X\n", kode, kode)
// Normalisasi: é composed (1 rune) vs decomposed (2 rune).
composed := "café"
decomposed := "cafe\u0301"
fmt.Printf("composed : %d byte, %d rune\n", len(composed), utf8.RuneCountInString(composed))
fmt.Printf("decomposed: %d byte, %d rune\n", len(decomposed), utf8.RuneCountInString(decomposed))
fmt.Println("tampilan :", composed, "|", decomposed)
fmt.Println("sama? :", composed == decomposed)
}go run utf8-demo.goOutput yang Dihasilkan:
"Halo" : 4 byte, 4 rune
"café" : 5 byte, 4 rune
"日本語": 9 byte, 3 rune
rune '日' mulai di byte 0
rune '本' mulai di byte 3
rune '語' mulai di byte 6
rune pertama '日' lebar 3 byte
rune 'é' = code point U+00E9
composed : 5 byte, 4 rune
decomposed: 6 byte, 5 rune
tampilan : café | café
sama? : falseMari kita telaah baris demi baris program di atas:
- Baris 10-16 (
fmt.Printftiga baris pertama): mencetaklendanRuneCountInStringuntuk tiga teks; selisih angka byte dan rune membuktikan lebar byte yang variabel. - Baris 19-21 (
for i, r := range "日本語"): loop mendekode tiap kanji; indeksimelompat 0 → 3 → 6 karena setiap kanji 3 byte, dan%qmenampilkan rune sebagai karakter. - Baris 24-25 (
utf8.DecodeRuneInString): mengurai rune pertama'日'beserta lebar3byte tanpa bantuan loop. - Baris 28-29 (
var kode rune = 'é'): menegaskan rune berupa angka —%04Xmencetak code pointU+00E9dalam notasi heksadesimal. - Baris 32-35 (bagian normalisasi): membandingkan bentuk composed dan decomposed; panjang byte 5 lawan 6, jumlah rune 4 lawan 5.
- Baris 36-37 (
fmt.Printlnterakhir): menampilkan kedua string yang tampak identik diikuti hasil==yangfalsekarena isi bytenya berbeda.
Pemahaman byte-rune ini adalah fondasi bagi seluruh pengolahan teks selanjutnya. Dengan model internal yang jelas, kita siap mengenal perangkat siap pakai untuk mencari, memotong, dan memformat teks pada chapter Package strings .
Referensi #
- Dokumentasi package unicode/utf8 — fungsi dekode dan penghitungan rune.
- Blog Go: Strings, bytes, runes and characters — tulisan klasik tim Go tentang model UTF-8.
- Spesifikasi Go: String types — definisi resmi string dan rune.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
utf8-demo.go
Unduh berkaspackage main
import (
"fmt"
"unicode/utf8"
)
func main() {
// Teks ASCII: satu karakter menempati tepat satu byte.
fmt.Printf("\"Halo\" : %d byte, %d rune\n", len("Halo"), utf8.RuneCountInString("Halo"))
// Huruf beraksen memakai 2 byte dalam UTF-8.
fmt.Printf("\"café\" : %d byte, %d rune\n", len("café"), utf8.RuneCountInString("café"))
// Kanji memakai 3 byte per karakter.
fmt.Printf("\"日本語\": %d byte, %d rune\n", len("日本語"), utf8.RuneCountInString("日本語"))
// range menghasilkan indeks byte dan rune, bukan nomor karakter.
for i, r := range "日本語" {
fmt.Printf("rune %q mulai di byte %d\n", r, i)
}
// DecodeRuneInString mengurai satu rune dari depan string.
r, lebar := utf8.DecodeRuneInString("日本語")
fmt.Printf("rune pertama %q lebar %d byte\n", r, lebar)
// rune adalah alias int32 yang menyimpan code point.
var kode rune = 'é'
fmt.Printf("rune %q = code point U+%04X\n", kode, kode)
// Normalisasi: é composed (1 rune) vs decomposed (2 rune).
composed := "café"
decomposed := "cafe\u0301"
fmt.Printf("composed : %d byte, %d rune\n", len(composed), utf8.RuneCountInString(composed))
fmt.Printf("decomposed: %d byte, %d rune\n", len(decomposed), utf8.RuneCountInString(decomposed))
fmt.Println("tampilan :", composed, "|", decomposed)
fmt.Println("sama? :", composed == decomposed)
}