| name | lang-go-dev |
| description | Foundational Go patterns covering types, interfaces, goroutines, channels, and common idioms. Use when writing Go code, understanding Go's concurrency model, or needing guidance on which specialized Go skill to use. This is the entry point for Go development. |
Go Fundamentals
Foundational Go patterns and core language features. This skill serves as both a reference for common patterns and an index to specialized Go skills.
Overview
┌─────────────────────────────────────────────────────────────────┐
│ Go Skill Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ │
│ │ lang-go-dev │ ◄── You are here │
│ │ (foundation) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌───────────────────┼───────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ concurrency │ │ testing │ │ modules │ │
│ │ patterns │ │ patterns │ │ packages │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
This skill covers:
- Basic types and zero values
- Structs, interfaces, and embedding
- Slices, maps, and iteration
- Error handling patterns
- Goroutines and channels basics
- Common idioms and conventions
This skill does NOT cover (see specialized skills):
- Advanced concurrency patterns →
go-concurrency-patterns - Module management and versioning → future skill
- Testing strategies → future skill
- Performance optimization → future skill
Quick Reference
| Task | Syntax |
|---|---|
| Variable declaration | var x int or x := 0 |
| Constant | const Pi = 3.14 |
| Function | func name(x int) int { return x } |
| Multiple return | func f() (int, error) |
| Struct | type User struct { Name string } |
| Interface | type Reader interface { Read() } |
| Slice | []int{1, 2, 3} |
| Map | map[string]int{"a": 1} |
| Goroutine | go func() { ... }() |
| Channel | ch := make(chan int) |
| Defer | defer file.Close() |
Skill Routing
Use this table to find the right specialized skill:
| When you need to... | Use this skill |
|---|---|
| Complex concurrency (worker pools, fan-out) | go-concurrency-patterns |
| Module versioning, go.mod management | future: lang-go-modules-dev |
| Testing strategies, mocking | future: lang-go-testing-dev |
| HTTP servers, middleware | future: lang-go-http-dev |
Types and Variables
Basic Types
// Boolean
var active bool = true
// Numeric types
var i int = 42 // Platform-dependent size
var i64 int64 = 42 // Explicit 64-bit
var f float64 = 3.14 // Default float type
var c complex128 = 1+2i // Complex numbers
// String (immutable UTF-8)
var s string = "hello"
// Byte and rune
var b byte = 'A' // Alias for uint8
var r rune = '世' // Alias for int32 (Unicode code point)
Zero Values
Every type has a zero value - no uninitialized variables in Go:
| Type | Zero Value |
|---|---|
bool |
false |
int, float64 |
0 |
string |
"" (empty string) |
pointer, slice, map, chan, func |
nil |
struct |
All fields zero-valued |
Variable Declaration
// Explicit type
var name string = "Alice"
// Type inference
var name = "Alice"
// Short declaration (inside functions only)
name := "Alice"
// Multiple variables
var x, y int = 1, 2
a, b := 1, "hello"
// Block declaration
var (
name string = "Alice"
age int = 30
active bool = true
)
Constants
// Typed constant
const Pi float64 = 3.14159
// Untyped constant (more flexible)
const MaxSize = 1024
// Constant block with iota
const (
Sunday = iota // 0
Monday // 1
Tuesday // 2
)
// Iota patterns
const (
_ = iota // Skip 0
KB = 1 << (10 * iota) // 1024
MB // 1048576
GB // 1073741824
)
Functions
Basic Functions
// Simple function
func greet(name string) string {
return "Hello, " + name
}
// Multiple parameters of same type
func add(x, y int) int {
return x + y
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Named return values
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // Naked return
}
Variadic Functions
// Accept any number of arguments
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Usage
sum(1, 2, 3)
sum([]int{1, 2, 3}...) // Spread slice
Function Values and Closures
// Functions are first-class values
var fn func(int) int
fn = func(x int) int { return x * 2 }
// Closure (captures outer variable)
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := counter()
c() // 1
c() // 2
Defer
// Deferred calls execute in LIFO order when function returns
func process(filename string) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close() // Guaranteed to run
// Process file...
return nil
}
// Arguments evaluated immediately, call deferred
func demo() {
for i := 0; i < 3; i++ {
defer fmt.Println(i) // Prints 2, 1, 0
}
}
Structs
Definition and Instantiation
// Define struct type
type User struct {
ID int
Name string
Email string
CreatedAt time.Time
}
// Create instances
u1 := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
u2 := User{1, "Alice", "alice@example.com", time.Now()} // Positional (fragile)
u3 := User{Name: "Bob"} // Other fields get zero values
// Pointer to struct
u4 := &User{Name: "Charlie"}
// Anonymous struct (one-off use)
point := struct {
X, Y int
}{10, 20}
Methods
// Value receiver (copy)
func (u User) FullName() string {
return u.Name
}
// Pointer receiver (can modify, avoids copy)
func (u *User) UpdateEmail(email string) {
u.Email = email
}
// Usage
user := User{Name: "Alice"}
user.UpdateEmail("new@example.com") // Automatic &user
When to Use Pointer vs Value Receiver
| Use Pointer Receiver | Use Value Receiver |
|---|---|
| Method modifies the receiver | Method only reads |
| Struct is large | Struct is small |
| Consistency with other methods | Immutability desired |
Embedding (Composition)
type Person struct {
Name string
Age int
}
func (p Person) Greet() string {
return "Hello, " + p.Name
}
type Employee struct {
Person // Embedded (not named)
EmployeeID string
Department string
}
// Employee "inherits" Person's fields and methods
emp := Employee{
Person: Person{Name: "Alice", Age: 30},
EmployeeID: "E001",
Department: "Engineering",
}
emp.Name // Promoted field
emp.Greet() // Promoted method
emp.Person.Name // Explicit access
Interfaces
Definition and Implementation
// Define interface
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// Compose interfaces
type ReadWriter interface {
Reader
Writer
}
// Implement implicitly (no "implements" keyword)
type FileReader struct {
data []byte
pos int
}
func (f *FileReader) Read(p []byte) (int, error) {
n := copy(p, f.data[f.pos:])
f.pos += n
return n, nil
}
// FileReader now implements Reader
Empty Interface
// interface{} accepts any type (like any in TS)
func printAnything(v interface{}) {
fmt.Println(v)
}
// Go 1.18+: 'any' is alias for interface{}
func printAnything(v any) {
fmt.Println(v)
}
Type Assertions
// Assert specific type
var i interface{} = "hello"
s := i.(string) // Panics if wrong type
s, ok := i.(string) // Safe: ok is false if wrong type
if s, ok := i.(string); ok {
fmt.Println(s)
}
Type Switch
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
Common Interfaces
| Interface | Methods | Use |
|---|---|---|
io.Reader |
Read([]byte) (int, error) |
Read data |
io.Writer |
Write([]byte) (int, error) |
Write data |
io.Closer |
Close() error |
Clean up resources |
fmt.Stringer |
String() string |
Custom string representation |
error |
Error() string |
Error type |
Collections
Arrays
// Fixed size, rarely used directly
var arr [5]int // Zero-valued
arr := [5]int{1, 2, 3, 4, 5}
arr := [...]int{1, 2, 3} // Size inferred (3)
// Arrays are values, not references
arr2 := arr // Full copy
Slices
// Dynamic, reference type (view into array)
var s []int // nil slice
s := []int{1, 2, 3} // Literal
s := make([]int, 5) // Length 5, capacity 5
s := make([]int, 0, 10) // Length 0, capacity 10
// Slice operations
len(s) // Length
cap(s) // Capacity
s[1:3] // Sub-slice [1, 3)
s[:3] // [0, 3)
s[1:] // [1, len)
// Append (may reallocate)
s = append(s, 4) // Single element
s = append(s, 4, 5, 6) // Multiple elements
s = append(s, other...) // Another slice
// Copy
dst := make([]int, len(src))
copy(dst, src)
Slice Gotchas
// Slices share underlying array
original := []int{1, 2, 3, 4, 5}
slice := original[1:3] // [2, 3]
slice[0] = 99 // Modifies original!
// Safe copy
safeCopy := make([]int, len(original[1:3]))
copy(safeCopy, original[1:3])
Maps
// Create maps
var m map[string]int // nil map (read ok, write panics)
m := map[string]int{} // Empty map
m := make(map[string]int) // Empty map
m := map[string]int{ // With values
"alice": 30,
"bob": 25,
}
// Operations
m["key"] = 42 // Set
val := m["key"] // Get (zero value if missing)
val, ok := m["key"] // Check existence
delete(m, "key") // Delete
len(m) // Size
// Iterate (order is random!)
for key, value := range m {
fmt.Println(key, value)
}
Iteration
// Range over slice
for i, v := range slice {
fmt.Println(i, v)
}
// Index only
for i := range slice {
fmt.Println(i)
}
// Value only
for _, v := range slice {
fmt.Println(v)
}
// Range over map
for k, v := range m {
fmt.Println(k, v)
}
// Range over string (runes)
for i, r := range "hello" {
fmt.Printf("%d: %c\n", i, r)
}
// Range over channel
for msg := range ch {
fmt.Println(msg)
}
Error Handling
The error Interface
// error is a built-in interface
type error interface {
Error() string
}
// Create errors
err := errors.New("something went wrong")
err := fmt.Errorf("failed to process %s: %w", name, originalErr)
Error Handling Pattern
// Check errors immediately
result, err := doSomething()
if err != nil {
return err // Or handle appropriately
}
// Use result...
// Multiple operations
f, err := os.Open(filename)
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
Custom Errors
// Simple custom error
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// Usage
return ValidationError{Field: "email", Message: "invalid format"}
Error Wrapping (Go 1.13+)
// Wrap error with context
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
// Check error type
if errors.Is(err, os.ErrNotExist) {
// Handle file not found
}
// Extract wrapped error
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path:", pathErr.Path)
}
Panic and Recover
// Panic for unrecoverable errors (avoid in library code)
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return n
}
// Recover from panic
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
fn()
return nil
}
Concurrency Basics
Goroutines
// Start goroutine
go doWork()
// Anonymous goroutine
go func() {
fmt.Println("In goroutine")
}()
// With arguments (captured by value)
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n)
}(i)
}
Channels
// Create channel
ch := make(chan int) // Unbuffered
ch := make(chan int, 10) // Buffered (capacity 10)
// Send and receive
ch <- 42 // Send (blocks if full/unbuffered)
val := <-ch // Receive (blocks if empty)
// Close channel
close(ch)
// Check if closed
val, ok := <-ch
if !ok {
fmt.Println("Channel closed")
}
// Range over channel (until closed)
for val := range ch {
fmt.Println(val)
}
Channel Directions
// Send-only channel
func producer(ch chan<- int) {
ch <- 42
}
// Receive-only channel
func consumer(ch <-chan int) {
val := <-ch
}
Select
// Wait on multiple channels
select {
case msg := <-ch1:
fmt.Println("From ch1:", msg)
case msg := <-ch2:
fmt.Println("From ch2:", msg)
case ch3 <- 42:
fmt.Println("Sent to ch3")
default:
fmt.Println("No communication ready")
}
// Timeout pattern
select {
case result := <-ch:
fmt.Println(result)
case <-time.After(time.Second):
fmt.Println("Timeout")
}
WaitGroup
import "sync"
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait() // Block until all done
Mutex
import "sync"
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// RWMutex for read-heavy workloads
type Cache struct {
mu sync.RWMutex
data map[string]string
}
func (c *Cache) Get(key string) string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.data[key]
}
Common Patterns
Options Pattern (Functional Options)
type Server struct {
host string
port int
timeout time.Duration
}
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func NewServer(host string, opts ...Option) *Server {
s := &Server{
host: host,
port: 8080, // Default
timeout: time.Minute, // Default
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer("localhost",
WithPort(9000),
WithTimeout(30*time.Second),
)
Constructor Pattern
type User struct {
id int
name string
}
// Constructor function (New prefix convention)
func NewUser(id int, name string) *User {
return &User{
id: id,
name: name,
}
}
// MustX for constructors that can fail
func MustCompile(pattern string) *regexp.Regexp {
re, err := regexp.Compile(pattern)
if err != nil {
panic(err)
}
return re
}
Table-Driven Tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Context for Cancellation
import "context"
func doWork(ctx context.Context) error {
select {
case <-time.After(time.Hour):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
// Usage with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := doWork(ctx); err != nil {
log.Println("Work cancelled:", err)
}
// Usage with cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second)
cancel() // Signal cancellation
}()
Go Conventions
Naming
| Type | Convention | Example |
|---|---|---|
| Exported (public) | PascalCase | UserService |
| Unexported (private) | camelCase | userCache |
| Acronyms | All caps | HTTPClient, xmlParser |
| Interfaces | -er suffix | Reader, Stringer |
| Getters | No Get prefix | Name() not GetName() |
| Package names | lowercase, single word | http, strconv |
Code Organization
// Standard file structure
package main
import (
"fmt" // Standard library
"net/http"
"github.com/pkg/errors" // Third-party
"myproject/internal/user" // Internal packages
)
// Constants
const MaxRetries = 3
// Package-level variables (minimize these)
var defaultClient = &http.Client{}
// Types
type Server struct { ... }
// Functions
func NewServer() *Server { ... }
// Methods
func (s *Server) Start() error { ... }
Error Strings
// Lowercase, no punctuation
errors.New("connection refused") // Good
errors.New("Connection refused.") // Bad
// With context
fmt.Errorf("open %s: %w", filename, err)
Troubleshooting
"declared and not used"
// Go requires all variables to be used
x := 5 // Error if x is never used
// Use blank identifier to ignore
x, _ := someFuncReturningTwo()
"cannot use X as type Y"
// Types must match exactly
var x int64 = 5
var y int = x // Error: int64 != int
// Explicit conversion required
var y int = int(x)
"nil pointer dereference"
// Check for nil before dereferencing
var u *User
fmt.Println(u.Name) // Panic!
if u != nil {
fmt.Println(u.Name)
}
"all goroutines are asleep - deadlock!"
// Common cause: unbuffered channel with no receiver
ch := make(chan int)
ch <- 1 // Blocks forever, no receiver
// Fix: use buffered channel or ensure receiver exists
ch := make(chan int, 1)
ch <- 1 // OK
"race condition detected"
// Run with race detector
// go run -race main.go
// Fix: use sync primitives
var mu sync.Mutex
mu.Lock()
shared++
mu.Unlock()
Module System
Go uses a module system for dependency management and package organization.
Package Basics
// Every .go file starts with package declaration
package main // Executable (has main function)
package user // Library package
// Package names should be:
// - lowercase, single word
// - match the directory name
// - not generic (utils, common, misc)
Import Paths
import (
// Standard library
"fmt"
"net/http"
"encoding/json"
// Third-party packages
"github.com/gorilla/mux"
"github.com/pkg/errors"
// Internal packages
"myproject/internal/config"
"myproject/pkg/utils"
)
// Aliased imports
import (
"fmt"
myfmt "myproject/fmt" // Avoid name collision
)
// Blank import (side effects only)
import _ "github.com/lib/pq" // Register database driver
// Dot import (avoid in production)
import . "fmt" // Allows Println() instead of fmt.Println()
Visibility
// Uppercase = exported (public)
// Lowercase = unexported (private)
package user
type User struct { // Exported
Name string // Exported
email string // Unexported (package-private)
}
func NewUser() *User { // Exported
return &User{}
}
func validate() bool { // Unexported
return true
}
Project Structure
myproject/
├── go.mod # Module definition
├── go.sum # Dependency checksums
├── main.go # Entry point (package main)
├── cmd/ # Multiple executables
│ ├── server/
│ │ └── main.go
│ └── cli/
│ └── main.go
├── pkg/ # Public library code
│ └── utils/
│ └── strings.go
├── internal/ # Private packages (enforced by Go)
│ └── database/
│ └── conn.go
└── api/ # API definitions (proto, OpenAPI)
Internal Packages
// Packages under 'internal/' are only importable by code
// within the parent of 'internal/'
// myproject/internal/database/conn.go
// Can be imported by: myproject/...
// Cannot be imported by: other projects
// This is enforced by the Go compiler
Serialization
Go uses struct tags for serialization configuration.
JSON Basics
import "encoding/json"
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Serialize (Marshal)
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
data, err := json.Marshal(user)
// {"name":"Alice","email":"alice@example.com","age":30}
// Pretty print
data, err := json.MarshalIndent(user, "", " ")
// Deserialize (Unmarshal)
var user User
err := json.Unmarshal([]byte(jsonStr), &user)
// Stream encoding/decoding
encoder := json.NewEncoder(writer)
encoder.Encode(user)
decoder := json.NewDecoder(reader)
decoder.Decode(&user)
Struct Tags
type Config struct {
// Rename field
Name string `json:"name"`
// Omit if empty/zero value
Email string `json:"email,omitempty"`
// Ignore field (never serialize)
Password string `json:"-"`
// String encoding for numbers
Count int `json:"count,string"`
// Multiple tags
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
Optional Fields
type User struct {
Name string `json:"name"`
Email *string `json:"email,omitempty"` // Pointer = nullable
Age int `json:"age,omitempty"` // Omit if 0
}
// Check for null
if user.Email != nil {
fmt.Println(*user.Email)
}
Custom Marshal/Unmarshal
type Status int
const (
StatusPending Status = iota
StatusActive
StatusDone
)
func (s Status) MarshalJSON() ([]byte, error) {
var str string
switch s {
case StatusPending:
str = "pending"
case StatusActive:
str = "active"
case StatusDone:
str = "done"
default:
return nil, fmt.Errorf("unknown status: %d", s)
}
return json.Marshal(str)
}
func (s *Status) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
switch str {
case "pending":
*s = StatusPending
case "active":
*s = StatusActive
case "done":
*s = StatusDone
default:
return fmt.Errorf("unknown status: %s", str)
}
return nil
}
Validation
// Using go-playground/validator
import "github.com/go-playground/validator/v10"
type User struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=150"`
}
var validate = validator.New()
func (u *User) Validate() error {
return validate.Struct(u)
}
// Usage
if err := user.Validate(); err != nil {
// Handle validation errors
for _, err := range err.(validator.ValidationErrors) {
fmt.Printf("%s: %s\n", err.Field(), err.Tag())
}
}
Other Formats
// YAML: gopkg.in/yaml.v3
import "gopkg.in/yaml.v3"
data, err := yaml.Marshal(config)
err = yaml.Unmarshal(data, &config)
// TOML: github.com/BurntSushi/toml
import "github.com/BurntSushi/toml"
_, err := toml.DecodeFile("config.toml", &config)
// XML: encoding/xml (standard library)
import "encoding/xml"
data, err := xml.Marshal(config)
See also: patterns-serialization-dev for cross-language serialization patterns
Build and Dependencies
Go uses Go Modules for dependency management.
go.mod
// go mod init myproject
module github.com/user/myproject
go 1.21
require (
github.com/gorilla/mux v1.8.0
github.com/pkg/errors v0.9.1
)
require (
// Indirect dependencies (automatically managed)
github.com/some/transitive v1.0.0 // indirect
)
// Replace directive (local development)
replace github.com/original/pkg => ../local/pkg
// Exclude problematic versions
exclude github.com/broken/pkg v1.0.0
Dependency Commands
| Command | Purpose |
|---|---|
go mod init <module> |
Initialize new module |
go mod tidy |
Add missing, remove unused deps |
go mod download |
Download dependencies |
go mod verify |
Verify dependency checksums |
go mod why <pkg> |
Explain why package is needed |
go mod graph |
Print dependency graph |
go mod vendor |
Copy deps to vendor/ |
Adding Dependencies
# Add dependency (latest version)
go get github.com/gorilla/mux
# Add specific version
go get github.com/gorilla/mux@v1.8.0
# Add latest of major version
go get github.com/gorilla/mux@v1
# Add from branch
go get github.com/gorilla/mux@main
# Update dependency
go get -u github.com/gorilla/mux
# Update all dependencies
go get -u ./...
# Remove dependency
go mod tidy # After removing imports
Build Commands
| Command | Purpose |
|---|---|
go build |
Compile package |
go build -o app |
Compile with output name |
go build ./... |
Build all packages |
go install |
Compile and install |
go run main.go |
Compile and run |
go clean |
Remove build artifacts |
Build Flags
# Cross-compilation
GOOS=linux GOARCH=amd64 go build -o app-linux
# Static binary (no CGO)
CGO_ENABLED=0 go build -o app
# Strip debug info (smaller binary)
go build -ldflags="-s -w" -o app
# Inject version info
go build -ldflags="-X main.Version=1.0.0" -o app
# Race detector
go build -race -o app
# Embed version
var Version = "dev" // Set by -ldflags
Workspaces (Go 1.18+)
# For multi-module development
go work init ./module1 ./module2
# go.work file
go 1.21
use (
./module1
./module2
)
Environment Variables
| Variable | Purpose |
|---|---|
GOPATH |
Workspace path (legacy) |
GOBIN |
Binary install location |
GOPROXY |
Module proxy URL |
GOPRIVATE |
Private module patterns |
GONOPROXY |
Skip proxy for patterns |
GONOSUMDB |
Skip checksum DB |
# Private repos
go env -w GOPRIVATE=github.com/mycompany/*
# Use default proxy with fallback
go env -w GOPROXY=https://proxy.golang.org,direct
Testing
Go has built-in testing support with the testing package.
Basic Tests
// user_test.go (must end with _test.go)
package user
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("Add(2, 3) = %d; want 5", result)
}
}
func TestAddNegative(t *testing.T) {
result := Add(-1, 1)
if result != 0 {
t.Errorf("Add(-1, 1) = %d; want 0", result)
}
}
Table-Driven Tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 1, 2, 3},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 1, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
Test Helpers
// Helper function
func setupTestDatabase(t *testing.T) *Database {
t.Helper() // Marks as helper (better error locations)
db, err := NewTestDatabase()
if err != nil {
t.Fatalf("setup failed: %v", err)
}
t.Cleanup(func() { // Cleanup after test
db.Close()
})
return db
}
func TestUser(t *testing.T) {
db := setupTestDatabase(t)
// Test using db...
}
Subtests and Parallel
func TestUser(t *testing.T) {
// Subtests
t.Run("Create", func(t *testing.T) {
t.Parallel() // Run in parallel
// Test create...
})
t.Run("Update", func(t *testing.T) {
t.Parallel()
// Test update...
})
}
Running Tests
# Run all tests
go test ./...
# Run specific package
go test ./pkg/user
# Run specific test
go test -run TestAdd ./...
# Run with verbose output
go test -v ./...
# Run with coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run with race detector
go test -race ./...
# Run benchmarks
go test -bench=. ./...
# Timeout
go test -timeout 30s ./...
# Skip long tests
go test -short ./...
Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
func BenchmarkAddParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
Add(1, 2)
}
})
}
// With setup
func BenchmarkProcess(b *testing.B) {
data := setupLargeData()
b.ResetTimer() // Don't count setup time
for i := 0; i < b.N; i++ {
Process(data)
}
}
Mocking with Interfaces
// Define interface for dependencies
type UserStore interface {
GetUser(id int) (*User, error)
SaveUser(user *User) error
}
// Real implementation
type PostgresStore struct { ... }
// Mock implementation
type MockStore struct {
users map[int]*User
}
func (m *MockStore) GetUser(id int) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return user, nil
}
// Test with mock
func TestService(t *testing.T) {
mock := &MockStore{
users: map[int]*User{
1: {ID: 1, Name: "Alice"},
},
}
service := NewUserService(mock)
user, err := service.GetUser(1)
// Assert...
}
Example Tests
// Example tests appear in documentation
func ExampleAdd() {
sum := Add(2, 3)
fmt.Println(sum)
// Output: 5
}
func ExampleUser_FullName() {
u := User{FirstName: "Alice", LastName: "Smith"}
fmt.Println(u.FullName())
// Output: Alice Smith
}
TestMain
func TestMain(m *testing.M) {
// Setup before all tests
setup()
// Run tests
code := m.Run()
// Cleanup after all tests
teardown()
os.Exit(code)
}
Metaprogramming
Go has limited metaprogramming capabilities compared to languages with macros. The primary approach is code generation.
Code Generation
//go:generate stringer -type=Status
type Status int
const (
StatusPending Status = iota
StatusActive
StatusDone
)
// Run: go generate ./...
// Generates: status_string.go with String() method
Common Generators
| Tool | Purpose |
|---|---|
stringer |
Generate String() for enums |
mockgen |
Generate mock implementations |
protoc |
Generate from Protocol Buffers |
go-bindata |
Embed files in binary |
sqlc |
Generate from SQL |
ent |
Generate ORM code |
Build Tags
// +build linux
// +build !windows
package mypackage
// This file only compiles on Linux, not on Windows
//go:build linux && amd64
package mypackage
// Go 1.17+ syntax
Reflection (Runtime)
import "reflect"
func PrintFields(v interface{}) {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
value := val.Field(i)
tag := field.Tag.Get("json")
fmt.Printf("%s (%s): %v [json:%s]\n",
field.Name, field.Type, value.Interface(), tag)
}
}
// Use reflection sparingly - it's slow and unsafe
See also: patterns-metaprogramming-dev for cross-language comparison
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
patterns-concurrency-dev- Goroutines, channels, select patternspatterns-serialization-dev- JSON, struct tags, validationpatterns-metaprogramming-dev- Code generation, build tags
References
- Go Documentation
- Effective Go
- Go by Example
- Specialized skills:
go-concurrency-patterns(external)