---
title: "Runes, Bytes, and Graphemes in Go"
date: 2025-08-09
tags: ["go"]
description: "Bytes, runes, and graphemes are not the same length: handling Tamil and emoji correctly in Go strings."
---

I once ran into this problem while handling names in Tamil and emoji in a Go web app: a string that looked short wasn't, and reversing it produced gibberish.

```mermaid
graph TD
    G["Grapheme Cluster\n(what users see)"]
    R1["Rune 1\n(code point)"]
    R2["Rune 2\n(combining mark)"]
    B1["Bytes\n(1-4 per rune)"]
    B2["Bytes\n(1-4 per rune)"]
    G --> R1
    G --> R2
    R1 --> B1
    R2 --> B2
```

## 1. Bytes

Go represents strings as immutable UTF-8 byte sequences.

```go
s := "வணக்கம்"
fmt.Println(len(s)) // 21
```

21 bytes, not visible symbols. Every Tamil character can span 3 bytes.

## 2. Runes

`string` to `[]rune` gives you code points.

```go
rs := []rune(s)
fmt.Println(len(rs)) // 7
```

7 runes, but some Tamil graphemes (like "க்") combine two runes: க + ்.

## 3. Grapheme clusters

Go's standard library stops at runes. For visible character units, use `github.com/rivo/uniseg`.

```go
for gr := uniseg.NewGraphemes(s); gr.Next(); {
    fmt.Printf("%q\n", gr.Str())
}
```

That outputs what a human reads: வ, ண, க், க, ம்.

```mermaid
graph LR
    T["வணக்கம் (Tamil)"]
    T --> |"len()"| BY["21 bytes"]
    T --> |"[]rune"| RU["7 runes"]
    T --> |"uniseg"| GR["5 graphemes"]
```

## Quick reference

| Task | Approach |
|------|----------|
| Count code points | `utf8.RuneCountInString(s)` |
| Count visible units | Grapheme iteration (uniseg) |
| Reverse text | Parse into graphemes, reverse slice, join |
| Slice safely | Only use `s[i:j]` on grapheme boundaries |
