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
147: Working with Byte Slices
By this point in the course, you're comfortable with strings and slices. But in the real world—especially when you're dealing with network sockets, file systems, or any kind of binary protocol—you'll find that []byte is actually the "first-class citizen" of Go. Strings are great for text, but byte slices are where the actual work happens.
Why bother with []byte when I have strings?
The short answer is mutability. In Go, strings are immutable. If you want to change a single character in a string, you can't; you have to create an entirely new string. That's a nightmare for performance if you're processing a 10MB file. A byte slice, however, is just a window into a mutable array of bytes.
I usually reach for []byte whenever I'm doing "buffer work." For example, if I'm cleaning up some raw data coming off a wire, I can modify the slice in place without triggering the garbage collector every two seconds.
package main
import "fmt"
func main() {
// Imagine this is a packet from a network socket
data := []byte("USER_ID:12345")
// I can change a byte directly by index
data[0] = 'A'
fmt.Printf("Modified: %s\n", data) // Output: AUSER_ID:12345
}
Is there a performance hit when I convert between the two?
Yes, there is. When you do b := []byte(s) or s := string(b), Go allocates new memory and copies the data. It does this to ensure that the immutability of the string is preserved. If you could convert a string to a slice without a copy, you could change the slice and accidentally change the "immutable" string, which would break a lot of assumptions in the language.
If you're doing this in a tight loop—say, parsing thousands of small packets per second—these allocations will add up and slow you down. In those cases, I try to keep the data as a []byte for as long as possible. Only convert to a string at the very end when you need to print it or put it into a map key.
How do I handle binary data that isn't just text?
Remember that byte is just an alias for uint8. This means a byte slice isn't just for "text that happens to be bytes"; it's for any raw binary data. When I'm working with custom binary protocols, I stop thinking in terms of characters and start thinking in terms of hex values.
Take a look at how you might verify a "Magic Byte"—a common technique used in file formats to identify the file type immediately.
package main
import "fmt"
func main() {
// A fake binary header: Magic Byte (0xAA), Version (0x01), Payload Length (0x04)
packet := []byte{0xAA, 0x01, 0x04, 0xDE, 0xAD, 0xBE, 0xEF}
if packet[0] != 0xAA {
fmt.Println("Invalid packet: Magic byte mismatch")
return
}
fmt.Printf("Packet version: %d, Payload length: %d\n", packet[1], packet[2])
}
Notice how I'm using hex literals like 0xAA. It's much more readable than using decimal numbers when you're dealing with raw bytes. If you see uint8 in a function signature, just know it's exactly the same thing as a byte.
📋 Practical Task
Build a Binary Packet Sanitizer
In this exercise, you will write a function that cleans up a raw byte slice. In some legacy protocols, "padding" bytes (null bytes 0x00) are added to the end of a message to fill a fixed-size buffer.
Your Task: Create a function called SanitizePacket that takes a []byte and returns a new slice with all trailing null bytes (0x00) removed. The function should not remove null bytes that appear in the middle of the data, only those at the very end.
Requirements:
- The function signature must be:
func SanitizePacket(data []byte) []byte - Test it with a slice like
[]byte{0x48, 0x69, 0x00, 0x21, 0x00, 0x00}. The result should be[]byte{0x48, 0x69, 0x00, 0x21}. - Avoid converting the slice to a string to perform the cleanup; work with the bytes directly.
There are no comments for now.