Pro Tips for Real-World Go
Choosing the Right Integer Typeβ
The right integer type depends on the context.
Core Principlesβ
package main
import (
"fmt"
"unsafe"
)
func main() {
// 1. General purpose (loops, counters, indices) β use int
for i := 0; i < 10; i++ {
_ = i
}
items := []string{"a", "b", "c"}
for i := range items {
fmt.Println(i, items[i])
}
// 2. Explicit size required β use int64 / int32
var fileSize int64 = 5_368_709_120 // 5 GB (exceeds int32 range)
var timestamp int64 = 1_700_000_000 // Unix timestamp
// 3. External protocols / binary β use size-guaranteed types
var ipByte uint8 = 192 // one byte of an IP address
var portNum uint16 = 8080 // port number
var fileFlag uint32 = 0x04000000 // file flag
// Check type sizes
fmt.Printf("int: %d bytes\n", unsafe.Sizeof(int(0)))
fmt.Printf("int32: %d bytes\n", unsafe.Sizeof(int32(0)))
fmt.Printf("int64: %d bytes\n", unsafe.Sizeof(int64(0)))
fmt.Println(fileSize, timestamp, ipByte, portNum, fileFlag)
}
When to Use Which Typeβ
| Situation | Recommended Type | Reason |
|---|---|---|
| Array index, loop counter | int | Language convention; slices use int indices |
| File size, memory size | int64 | Can handle over 2 GB |
| Unix timestamp | int64 | Avoids the year 2038 problem |
| Network port | uint16 | Protocol definition (0β65535) |
| Byte-level data | uint8 / byte | Single-byte processing |
| Bit flags | uint32 / uint64 | Bitwise ops without negatives |
| Count / size return values | int | Standard library convention |
float32 vs float64 β The Floating-Point Comparison Trapβ
Why You Should Choose float64β
package main
import (
"fmt"
"math"
)
func main() {
// float32 has significant precision loss
var f32 float32 = 0.1 + 0.2
var f64 float64 = 0.1 + 0.2
fmt.Printf("float32: %.20f\n", f32) // 0.30000001192092895508
fmt.Printf("float64: %.20f\n", f64) // 0.30000000000000004441
// Never use == to compare floating-point numbers
fmt.Println(f64 == 0.3) // false!
// Epsilon comparison (recommended)
const epsilon = 1e-9
almostEqual := func(a, b float64) bool {
return math.Abs(a-b) < epsilon
}
fmt.Println(almostEqual(0.1+0.2, 0.3)) // true
// Relative epsilon (more accurate for large numbers)
relativeEqual := func(a, b, eps float64) bool {
if a == b {
return true
}
diff := math.Abs(a - b)
avg := (math.Abs(a) + math.Abs(b)) / 2
return diff/avg < eps
}
a, b := 1_000_000.0, 1_000_000.001
fmt.Println(relativeEqual(a, b, 1e-6)) // true (relative error < 1 ppm)
}
When to Use float32β
package main
import (
"fmt"
"math"
"unsafe"
)
func main() {
// Use float32 only when memory is critical
// Examples: 3D graphics, ML weights, large numeric arrays
// Memory comparison: float64 vs float32 arrays
f64Array := [1_000_000]float64{}
f32Array := [1_000_000]float32{}
fmt.Printf("float64 array: %d MB\n", unsafe.Sizeof(f64Array)/1024/1024) // 8 MB
fmt.Printf("float32 array: %d MB\n", unsafe.Sizeof(f32Array)/1024/1024) // 4 MB
// Go's math package is float64-based
// Using float32 with math functions requires two conversions
var f float32 = 3.14
result := float32(math.Sin(float64(f))) // two conversions needed
fmt.Println(result)
}
Detecting Integer Overflowβ
Go does not automatically detect integer overflow. You must check manually or use the math/bits package.
package main
import (
"fmt"
"math"
"math/bits"
)
func main() {
// Overflow happens silently (wraps around)
var x int8 = 127
x++
fmt.Println(x) // -128 (overflow!)
// Manual overflow check for addition
safeAdd := func(a, b int64) (int64, bool) {
if b > 0 && a > math.MaxInt64-b {
return 0, false // overflow
}
if b < 0 && a < math.MinInt64-b {
return 0, false // underflow
}
return a + b, true
}
result, ok := safeAdd(math.MaxInt64, 1)
fmt.Printf("MaxInt64 + 1: %d, ok: %v\n", result, ok) // 0, false
result, ok = safeAdd(100, 200)
fmt.Printf("100 + 200: %d, ok: %v\n", result, ok) // 300, true
// Using math/bits (Go 1.9+)
a, b := uint64(math.MaxUint64), uint64(1)
sum, carry := bits.Add64(a, b, 0)
fmt.Printf("MaxUint64 + 1 = %d, carry = %d\n", sum, carry) // 0, 1
// Multiplication overflow check
hi, lo := bits.Mul64(1_000_000_000, 1_000_000_000)
fmt.Printf("1e9 * 1e9: hi=%d, lo=%d\n", hi, lo) // hi=0, lo=10^18
}
String Memory Optimizationβ
strings.Builder vs bytes.Buffer vs []byteβ
package main
import (
"bytes"
"fmt"
"strings"
"time"
)
const iterations = 100_000
func withStringPlus() string {
s := ""
for i := 0; i < iterations; i++ {
s += "x"
}
return s
}
func withStringsBuilder() string {
var b strings.Builder
b.Grow(iterations) // pre-allocate capacity (optional but helpful)
for i := 0; i < iterations; i++ {
b.WriteByte('x')
}
return b.String()
}
func withBytesBuffer() string {
var b bytes.Buffer
b.Grow(iterations)
for i := 0; i < iterations; i++ {
b.WriteByte('x')
}
return b.String()
}
func withByteSlice() string {
b := make([]byte, 0, iterations)
for i := 0; i < iterations; i++ {
b = append(b, 'x')
}
return string(b)
}
func benchmark(name string, fn func() string) {
start := time.Now()
_ = fn()
fmt.Printf("%-25s %v\n", name+":", time.Since(start))
}
func main() {
benchmark("string + (plus)", withStringPlus)
benchmark("strings.Builder", withStringsBuilder)
benchmark("bytes.Buffer", withBytesBuffer)
benchmark("[]byte + string()", withByteSlice)
// Advanced strings.Builder usage
var sb strings.Builder
sb.Grow(256) // pre-allocate expected size to avoid reallocation
items := []string{"apple", "banana", "cherry"}
for i, item := range items {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(item)
}
fmt.Println(sb.String()) // apple, banana, cherry
}
[]byte Reuse Patternβ
package main
import (
"fmt"
"strings"
)
func processLines(lines []string) []string {
// Reuse the buffer to reduce GC pressure
buf := make([]byte, 0, 256) // initial capacity 256 bytes
results := make([]string, 0, len(lines))
for _, line := range lines {
buf = buf[:0] // reset length to 0 (capacity is retained)
trimmed := strings.TrimSpace(line)
buf = append(buf, strings.ToUpper(trimmed)...)
results = append(results, string(buf))
}
return results
}
func main() {
lines := []string{
" hello world ",
" go language ",
" programming ",
}
results := processLines(lines)
for _, r := range results {
fmt.Println(r)
}
}
Struct Field Alignment β Saving Memoryβ
CPUs read memory most efficiently when values are aligned to their size (on a 64-bit system, that means 8-byte boundaries). The Go compiler inserts padding between struct fields to satisfy alignment requirements. Ordering fields from largest to smallest reduces padding and saves memory.
package main
import (
"fmt"
"unsafe"
)
// Inefficient layout: lots of padding
type BadLayout struct {
A bool // 1 byte + 7 bytes padding
B float64 // 8 bytes
C bool // 1 byte + 7 bytes padding
D int32 // 4 bytes + 4 bytes padding
}
// Efficient layout: largest types first, smallest last
type GoodLayout struct {
B float64 // 8 bytes
D int32 // 4 bytes
A bool // 1 byte
C bool // 1 byte + 2 bytes padding
}
func main() {
bad := BadLayout{}
good := GoodLayout{}
fmt.Printf("BadLayout size: %d bytes\n", unsafe.Sizeof(bad)) // 32 bytes
fmt.Printf("GoodLayout size: %d bytes\n", unsafe.Sizeof(good)) // 16 bytes
// Check the offset of each field
fmt.Println("\nBadLayout field offsets:")
fmt.Printf(" A(bool): %d\n", unsafe.Offsetof(bad.A))
fmt.Printf(" B(float64): %d\n", unsafe.Offsetof(bad.B))
fmt.Printf(" C(bool): %d\n", unsafe.Offsetof(bad.C))
fmt.Printf(" D(int32): %d\n", unsafe.Offsetof(bad.D))
fmt.Println("\nGoodLayout field offsets:")
fmt.Printf(" B(float64): %d\n", unsafe.Offsetof(good.B))
fmt.Printf(" D(int32): %d\n", unsafe.Offsetof(good.D))
fmt.Printf(" A(bool): %d\n", unsafe.Offsetof(good.A))
fmt.Printf(" C(bool): %d\n", unsafe.Offsetof(good.C))
// The difference at scale
nElements := 1_000_000
badSlice := make([]BadLayout, nElements)
goodSlice := make([]GoodLayout, nElements)
fmt.Printf("\nOne million elements:\n")
fmt.Printf(" BadLayout: %d MB\n", unsafe.Sizeof(badSlice[0])*uintptr(nElements)/1024/1024)
fmt.Printf(" GoodLayout: %d MB\n", unsafe.Sizeof(goodSlice[0])*uintptr(nElements)/1024/1024)
}
Output:
BadLayout size: 32 bytes
GoodLayout size: 16 bytes
The Cost of string([]byte) Conversionβ
Every conversion between string and []byte causes a memory copy. Use the following patterns to avoid unnecessary conversions in hot paths.
package main
import (
"bytes"
"fmt"
"strings"
)
func main() {
data := []byte("hello world")
// Bad: conversion overhead
if string(data) == "hello world" {
fmt.Println("match (with conversion)")
}
// Good: use bytes package directly
if bytes.Equal(data, []byte("hello world")) {
fmt.Println("match (no conversion)")
}
// Practical example: parsing an HTTP header
header := []byte("Content-Type: application/json")
colonIdx := strings.IndexByte(string(header), ':')
if colonIdx >= 0 {
key := strings.TrimSpace(string(header[:colonIdx]))
value := strings.TrimSpace(string(header[colonIdx+1:]))
fmt.Printf("key: %q, value: %q\n", key, value)
}
}
Handling Large Numbers β math/bigβ
When values exceed the range of built-in integer types, use the math/big package.
package main
import (
"fmt"
"math/big"
)
func main() {
// BigInt β arbitrary-precision integers
a, _ := new(big.Int).SetString("123456789012345678901234567890", 10)
b, _ := new(big.Int).SetString("987654321098765432109876543210", 10)
sum := new(big.Int).Add(a, b)
product := new(big.Int).Mul(a, b)
fmt.Println("sum: ", sum)
fmt.Println("product:", product)
// Factorial (int64 overflows after 20!)
factorial := func(n int64) *big.Int {
result := big.NewInt(1)
for i := int64(2); i <= n; i++ {
result.Mul(result, big.NewInt(i))
}
return result
}
f100 := factorial(100).String()
fmt.Printf("100! = %s...\n", f100[:20]) // first 20 digits
// BigFloat β arbitrary-precision floating-point
pi := new(big.Float).SetPrec(256) // 256-bit precision
pi.SetString("3.14159265358979323846264338327950288")
fmt.Printf("Ο (256-bit): %s\n", pi.Text('f', 30)) // 30 decimal places
}
Compile-Time Computation with Constant Expressionsβ
package main
import (
"fmt"
"time"
)
const (
// Computed at compile time β zero runtime cost
KB = 1 << 10 // 1024
MB = 1 << 20 // 1048576
GB = 1 << 30 // 1073741824
MaxPacketSize = 64 * KB // 65536
DefaultBuffer = 4 * MB // 4194304
MaxFileSize = 2*GB - 1 // 2147483647
// Time constants
OneDay = 24 * time.Hour
OneWeek = 7 * OneDay
)
func main() {
fmt.Printf("MaxPacketSize: %d bytes (%.1f KB)\n",
MaxPacketSize, float64(MaxPacketSize)/KB)
fmt.Printf("DefaultBuffer: %d bytes (%.1f MB)\n",
DefaultBuffer, float64(DefaultBuffer)/MB)
fmt.Printf("MaxFileSize: %d bytes (%.1f GB)\n",
MaxFileSize, float64(MaxFileSize)/GB)
fmt.Printf("OneDay: %v\n", OneDay) // 24h0m0s
fmt.Printf("OneWeek: %v\n", OneWeek) // 168h0m0s
timeout := 5 * time.Second
fmt.Println("timeout:", timeout)
}
Creating Meaningful Types with Type Definitionsβ
Type definitions are not just aliases β they are a powerful tool for expressing domain concepts in code.
package main
import (
"fmt"
"strings"
)
// Meaningful domain types
type UserID int64
type OrderID int64
type ProductID int64
type Email string
type Cents int64 // represent money as integer cents
func (e Email) IsValid() bool {
return strings.Contains(string(e), "@") &&
strings.Contains(string(e), ".")
}
func (e Email) Domain() string {
parts := strings.Split(string(e), "@")
if len(parts) != 2 {
return ""
}
return parts[1]
}
func (c Cents) ToDollars() float64 {
return float64(c) / 100
}
func (c Cents) String() string {
return fmt.Sprintf("$%.2f", c.ToDollars())
}
// The function signature is self-documenting β swapping uid and oid is a compile error
func processOrder(uid UserID, oid OrderID, amount Cents) {
fmt.Printf("user %d, order %d: processing %s\n", uid, oid, amount)
}
func main() {
uid := UserID(1001)
oid := OrderID(5001)
price := Cents(2999) // $29.99
processOrder(uid, oid, price)
// processOrder(int64(oid), int64(uid), price) // compile error!
email := Email("user@example.com")
fmt.Printf("email valid: %v\n", email.IsValid())
fmt.Printf("domain: %s\n", email.Domain())
total := Cents(1500 + 750 + 299)
fmt.Printf("total: %s\n", total) // $25.49
}
Real-World Anti-Patternsβ
Anti-pattern 1: Using int instead of boolβ
// Bad: C style
func isActiveOld(status int) int {
return status // 0 = inactive, 1 = active (ambiguous)
}
// Good: explicit bool
func isActive(status string) bool {
return status == "active"
}
// Better: meaningful type
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusBanned UserStatus = "banned"
)
func (s UserStatus) IsActive() bool {
return s == UserStatusActive
}
Anti-pattern 2: Unnecessary string β []byte conversionsβ
package main
import (
"bytes"
"fmt"
"strings"
)
func main() {
data := []byte("Hello, World!")
// Bad: two conversions
if strings.Contains(string(data), "World") {
fmt.Println("found (with conversion)")
}
// Good: use bytes package directly
if bytes.Contains(data, []byte("World")) {
fmt.Println("found (no conversion)")
}
// Bad: repeated conversions inside a loop
messages := [][]byte{
[]byte("hello"),
[]byte("world"),
[]byte("go"),
}
var result1 []string
for _, msg := range messages {
result1 = append(result1, strings.ToUpper(string(msg)))
}
// Good: use bytes.ToUpper, convert only once at the end
var result2 []string
for _, msg := range messages {
upper := bytes.ToUpper(msg)
result2 = append(result2, string(upper))
}
fmt.Println(result1)
fmt.Println(result2)
}
Anti-pattern 3: Using float for monetary calculationsβ
package main
import (
"fmt"
"math/big"
)
func main() {
// Bad: float64 for money
price := 0.1
quantity := 3
totalBad := price * float64(quantity)
fmt.Printf("float64 total: %.20f\n", totalBad) // 0.30000000000000004441
// Good option 1: use integers (cents)
priceCents := 10 // 10 cents = $0.10
totalCents := priceCents * quantity
fmt.Printf("integer total: %d cents = $%.2f\n", totalCents, float64(totalCents)/100)
// Good option 2: use math/big.Float
priceF := new(big.Float).SetPrec(64).SetFloat64(0.1)
quantityF := new(big.Float).SetPrec(64).SetInt64(int64(quantity))
totalBigF := new(big.Float).Mul(priceF, quantityF)
fmt.Printf("big.Float total: %s\n", totalBigF.Text('f', 10))
}
Anti-pattern 4: Overusing global variablesβ
package main
import "fmt"
// Bad: global state β dangerous for testing and concurrency
var globalConfig struct {
Host string
Port int
}
// Good: pass configuration via structs or function arguments
type Config struct {
Host string
Port int
}
type Server struct {
config Config
}
func NewServer(cfg Config) *Server {
return &Server{config: cfg}
}
func (s *Server) Address() string {
return fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
}
func main() {
cfg := Config{Host: "localhost", Port: 8080}
srv := NewServer(cfg)
fmt.Println("server address:", srv.Address())
}
Ch2 Key Takeawaysβ
| Topic | Key Point |
|---|---|
| Variable declaration | := inside functions only; undeclared variables use zero values |
| Type system | No implicit conversions; always use explicit T(v) |
| Constants | Use iota for enumerations; untyped constants are more flexible |
| Strings | Use range for rune iteration; use strings.Builder for efficient concatenation |
| Memory | Align struct fields; reuse []byte; prefer float64 |
| Type definitions | Create domain types for compile-time safety |