Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
187: utf8 Package for Rune Handling
I've seen this mistake more times than I can count, even from developers who have been using Go for a while: assuming that len(str) tells you how many characters are in a string. It feels intuitive, right? If you have a word, you want to know how long it is. But in Go, strings are read-only slices of bytes, not slices of characters.
The "len() equals character count" trap
Let's look at a concrete example. Suppose you're building a username validator and you want to make sure a name isn't too long. You might write something like this:
package main
import "fmt"
func main() {
username := "Gopher ʕ•ᴥ•ʔ"
fmt.Println("Length:", len(username))
}
If you run this, you'll expect to see 13 (the number of visual characters). Instead, you'll get 21. Why? Because the "bear" emojis and special characters in that string are encoded using multiple bytes in UTF-8. len() is simply counting those bytes. If you use this for a database column limit or a UI constraint, you're going to end up cutting users off mid-character, which results in those ugly replacement characters (like ) appearing in your app.
Counting actual runes with the utf8 package
To get the actual number of Unicode code points—which we call runes in Go—you need the unicode/utf8 package. I always reach for utf8.RuneCountInString() when the visual length is what actually matters for the business logic.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
username := "Gopher ʕ•ᴥ•ʔ"
byteLen := len(username)
runeLen := utf8.RuneCountInString(username)
fmt.Printf("Bytes: %d\n", byteLen) // 21
fmt.Printf("Runes: %d\n", runeLen) // 13
}
Notice that RuneCountInString has to iterate through the string to decode the UTF-8 sequences, making it an O(n) operation, whereas len() is O(1) because it just reads the slice header. I usually tell my juniors: use len() for memory allocation and buffer sizing, but use utf8 for anything the user actually sees.
Ensuring your bytes actually make sense
Beyond just counting, there's a risk when you're receiving data from an external API or a raw TCP socket. Go doesn't force strings to be valid UTF-8; a string is just a sequence of bytes. You can technically cast a slice of random garbage bytes into a string, and Go won't complain—until you try to print it or process it.
This is where utf8.ValidString() becomes your best friend. I use this as a guard clause at the edge of my system to reject malformed data before it hits my internal logic.
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
// A slice of bytes that is NOT valid UTF-8
badBytes := []byte{0xff, 0xfe, 0xfd}
badString := string(badBytes)
if !utf8.ValidString(badString) {
fmt.Println("Warning: This string contains invalid UTF-8 sequences!")
}
}
If you're doing heavy-duty manipulation—like manually decoding a stream of bytes one character at a time—you can also use utf8.DecodeRuneInString(). It returns the first rune and its size in bytes, allowing you to "step" through a string without converting the whole thing into a []rune slice (which would allocate a whole new piece of memory).
📋 Practical Task
Build a Unicode-Aware Character Limiter
You are tasked with writing a function for a social media profile bio. The requirement is that the bio must be between 10 and 100 characters (runes), regardless of how many bytes those characters take up. Additionally, the function must reject the input entirely if it contains invalid UTF-8 sequences.
Implement a function with the following signature:
func ValidateBio(bio string) (bool, error)
Requirements:
- Use
utf8.ValidStringto ensure the input is valid UTF-8. If not, returnfalseand an error. - Use
utf8.RuneCountInStringto check the length. - Return
true, nilif the length is between 10 and 100 inclusive. - Return
false, nilif the length is outside that range.
Test your function with these cases:
"Hello World!"(Valid, within range)"Hi"(Valid, too short)"Gopher ʕ•ᴥ•ʔ is a very happy little creature that loves Go!"(Valid, within range)- A string created from invalid bytes:
string([]byte{0xff, 0xfe, 0xfd})(Invalid UTF-8)
There are no comments for now.