Skip to main content

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​

SituationRecommended TypeReason
Array index, loop counterintLanguage convention; slices use int indices
File size, memory sizeint64Can handle over 2 GB
Unix timestampint64Avoids the year 2038 problem
Network portuint16Protocol definition (0–65535)
Byte-level datauint8 / byteSingle-byte processing
Bit flagsuint32 / uint64Bitwise ops without negatives
Count / size return valuesintStandard 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​

TopicKey Point
Variable declaration:= inside functions only; undeclared variables use zero values
Type systemNo implicit conversions; always use explicit T(v)
ConstantsUse iota for enumerations; untyped constants are more flexible
StringsUse range for rune iteration; use strings.Builder for efficient concatenation
MemoryAlign struct fields; reuse []byte; prefer float64
Type definitionsCreate domain types for compile-time safety