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:

Tekslen (byte)Jumlah runeKarakteristik
"Halo"44ASCII, 1 byte per karakter
"café"54é menempati 2 byte
"日本語"93Kanji CJK, 3 byte per karakter
"🌏"41Emoji, 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:

go
fmt.Println(len("日本語"), utf8.RuneCountInString("日本語"))

Output yang Dihasilkan:

text
9 3

Angka 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:

go
for i, r := range "café" {
	fmt.Println(i, r)
}

Output yang Dihasilkan:

text
0 99
1 97
2 102
3 233

Indeks 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:

go
fmt.Printf("%q %q\n", "caf\u00E9", "cafe\u0301")

Output yang Dihasilkan:

text
"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:

go
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)
}
bash
go run utf8-demo.go

Output yang Dihasilkan:

text
"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?    : false

Mari kita telaah baris demi baris program di atas:

  1. Baris 10-16 (fmt.Printf tiga baris pertama): mencetak len dan RuneCountInString untuk tiga teks; selisih angka byte dan rune membuktikan lebar byte yang variabel.
  2. Baris 19-21 (for i, r := range "日本語"): loop mendekode tiap kanji; indeks i melompat 0 → 3 → 6 karena setiap kanji 3 byte, dan %q menampilkan rune sebagai karakter.
  3. Baris 24-25 (utf8.DecodeRuneInString): mengurai rune pertama '日' beserta lebar 3 byte tanpa bantuan loop.
  4. Baris 28-29 (var kode rune = 'é'): menegaskan rune berupa angka — %04X mencetak code point U+00E9 dalam notasi heksadesimal.
  5. Baris 32-35 (bagian normalisasi): membandingkan bentuk composed dan decomposed; panjang byte 5 lawan 6, jumlah rune 4 lawan 5.
  6. Baris 36-37 (fmt.Println terakhir): menampilkan kedua string yang tampak identik diikuti hasil == yang false karena 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 #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

utf8-demo.go

Unduh berkas
go
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)
}