Basics, Types & Structs, Interfaces, Goroutines & Channels, Error Handling, Packages & Modules, Testing — Go mastery.
package main
import (
"fmt"
"math/rand"
"time"
)
// ── Variables ──
var x int = 10 // package-level, zero value if no init
var name string // zero value: ""
var isActive bool // zero value: false
var price float64 // zero value: 0.0
// Short declaration (only inside functions)
y := 20 // type inferred as int
msg := "hello" // type inferred as string
const Pi = 3.14 // constant, untyped or typed
const Timeout = 5 * time.Second
// Multiple declaration
var (
a, b int
c = "hello"
d float64
)
// ── Types ──
// Basic: bool, string, int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64,
// float32, float64, complex64, complex128
// Alias: byte = uint8, rune = int32 (Unicode code point)
// ── Functions ──
func add(a, b int) int {
return a + b
}
// Multiple return values
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Named return values
func rectArea(w, h float64) (area float64) {
area = w * h // "naked" return
return
}
// Variadic parameters
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
// Anonymous function (closure)
greet := func(name string) string {
return "Hello, " + name
}
fmt.Println(greet("Go"))
// Function as type
type Transformer func(int) int
func apply(nums []int, t Transformer) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = t(n)
}
return result
}
// Defer — runs when function returns (LIFO order)
func example() {
defer fmt.Println("3 — last")
defer fmt.Println("2 — second")
fmt.Println("1 — first")
// Output: 1, 2, 3
}
// Init function — runs before main (per file)
func init() {
rand.Seed(time.Now().UnixNano())
}
func main() {
// Control flow
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
// Switch (no break needed, auto-breaks)
switch day {
case "Monday", "Friday":
fmt.Println("workday")
default:
fmt.Println("other")
}
// Type switch
switch v := interface{}(42).(type) {
case int:
fmt.Println("integer:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Printf("unknown type %T\n", v)
}
// For loop
for i := 0; i < 10; i++ { }
for i := range 10 { } // 0 to 9
for _, v := range slice { } // iterate values
for i, v := range slice { } // index and value
for k, v := range myMap { } // map key-value
for i := 0; i < 10; i++ {
if i == 3 { continue }
if i == 7 { break }
}
}| Type | Zero Value |
|---|---|
| int, float64 | 0 |
| bool | false |
| string | "" (empty string) |
| pointer | nil |
| slice | nil |
| map | nil |
| channel | nil |
| function | nil |
| interface | nil |
| struct | all fields set to zero values |
| array | all elements set to zero values |
| Operation | Description | Example |
|---|---|---|
| &x | Address of x | ptr := &x |
| *ptr | Value at address | val := *ptr |
| new(Type) | Allocate pointer | p := new(int) |
| nil | Null pointer | if ptr == nil |
| -> | Struct field via pointer | ptr.field (auto-deref) |
| (*T)(nil) | Pointer to type | (*int)(nil) |
// ── Struct ──
type Person struct {
Name string
Age int
Email string
active bool // unexported (lowercase)
}
// Struct literal
p := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "bob@example.com", true}
// Anonymous struct
point := struct{ X, Y int }{3, 4}
// Nested struct
type Address struct {
Street string
City string
Country string
}
type Employee struct {
Person
Address
Role string // promoted field
}
emp := Employee{
Person: Person{Name: "Alice", Age: 30},
Address: Address{Street: "123 Main", City: "NYC"},
Role: "Engineer",
}
emp.Name // promoted from Person
emp.Street // promoted from Address
// ── Methods ──
type Rectangle struct {
Width, Height float64
}
// Value receiver (copy of struct)
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Pointer receiver (modifies original)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
r := Rectangle{10, 5}
fmt.Println(r.Area()) // 50
r.Scale(2)
fmt.Println(r.Area()) // 200
}
// ── Arrays and Slices ──
// Array (fixed size, value type)
var arr [5]int = [5]int{1, 2, 3, 4, 5}
arr2 := [...]int{1, 2, 3} // compiler counts
// Slice (dynamic, reference type)
slice := []int{1, 2, 3, 4, 5}
slice := make([]int, 5, 10) // len=5, cap=10
slice := make([]string, 0)
slice = append(slice, 6) // append
slice = append(slice, 7, 8, 9) // append multiple
s1 := slice[1:3] // [2, 3] (half-open)
s2 := slice[:3] // [1, 2, 3]
s3 := slice[2:] // [3, 4, 5, 6, 7, 8, 9]
copy(dst, src) // built-in copy
// Slice of slice (shares underlying array!)
sub := slice[1:3]
sub[0] = 999 // This modifies original slice!
// ── Maps ──
m := make(map[string]int)
m := map[string]int{"alice": 30, "bob": 25}
m["charlie"] = 35
age, exists := m["alice"] // exists is bool
delete(m, "bob")
for k, v := range m {
fmt.Printf("%s: %d\n", k, v)
}
// ── Type Aliases ──
type ID string
type Celsius float64
// ── Embedded Interfaces (type sets) ──
type Number interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
}
func Double[N Number](v N) N {
return v * 2
}
// ── Generics (Go 1.18+) ──
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
v := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return v, true
}| Feature | Array | Slice |
|---|---|---|
| Size | Fixed (compile-time) | Dynamic (runtime) |
| Type | Value type (copied) | Reference type (pointer to backing array) |
| Comparable | Yes (== compares elements) | No (== compares pointers) |
| Zero value | [N]T{zero values} | nil |
| Len/Cap | len == cap == N | len <= cap |
| Use case | Fixed-size buffers, matrices | Dynamic collections (99% of cases) |
// ── Interface (implicit implementation) ──
type Reader interface {
Read(p []byte) (n int, err error)
Close() error
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ── Composition (interface embedding) ──
type ReadWriter interface {
Reader
Writer
}
// ── Type assertion ──
var i interface{} = "hello"
s := i.(string) // panics if wrong type
s, ok := i.(string) // safe assertion, ok is bool
// Type switch
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
default:
fmt.Printf("unknown %T\n", v)
}
// ── Empty interface (any) ──
// interface{} or "any" (Go 1.18+)
func PrintAny(v any) {
fmt.Printf("value: %v, type: %T\n", v, v)
}
// ── Interface with type parameter (Go 1.18+) ──
type Stringer interface {
String() string
}
func Describe[T Stringer](v T) string {
return v.String()
}
// ── Comparable constraint ──
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
// ── Custom error type ──
type AppError struct {
Code int
Message string
}
func (e *AppError) Error() string {
return fmt.Sprintf("code=%d: %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return fmt.Errorf("context: %w", e.Message)
}
// Usage
func process() error {
return &AppError{Code: 404, Message: "not found"}
}
// ── Errors.Is / Errors.As ──
var err error = process()
var appErr *AppError
if errors.Is(err, io.EOF) {
fmt.Println("EOF")
}
if errors.As(err, &appErr) {
fmt.Printf("AppError code=%d\n", appErr.Code)
}
// ── Sort with custom comparator ──
sort.Slice(people, func(i, j int) bool {
return people[i].Age < people[j].Age
})
// Sort with slices package (Go 1.21+) ──
slices.Sort(people)
slices.SortFunc(people, func(a, b Person) int {
return strings.Compare(a.Name, b.Name)
})| Feature | Interface | Struct |
|---|---|---|
| Implementation | Implicit (duck typing) | Explicit (named receiver) |
| Methods | Method set only (no data) | Methods + data fields |
| Zero value | nil | Empty struct {} |
| Comparable | nil == nil, interface == nil trap! | Compared field-by-field |
| Composition | Embed interfaces | Embed structs |
| Use case | Behavior contracts | Data + behavior bundles |
// BAD — returns non-nil interface
func bad() error {
var p *AppError // p is nil
return p // interface has type *AppError, value nil -> NOT nil
}
// GOOD — returns nil interface
func good() error {
return nil
}// ── Goroutine ──
go func() {
fmt.Println("Running in background")
}()
// Wait for goroutine to finish
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ── Channel ──
ch := make(chan int) // unbuffered (blocking send/receive)
ch := make(chan int, 10) // buffered (non-blocking until full)
ch <- 42 // send (blocks if unbuffered and no receiver)
val := <-ch // receive (blocks if empty)
val, ok := <-ch // receive with ok (false if closed)
close(ch) // close channel (no more sends)
for v := range ch { // iterate until closed
fmt.Println(v)
}
// ── Channel Directions ──
func producer(out chan<- int) { // send-only
out <- 42
}
func consumer(in <-chan int) { // receive-only
val := <-in
}
func pipeline(in <-chan int, out chan<- int) {
// receives from in, sends to out
}
// ── Select (multiplex channels) ──
select {
case val := <-ch1:
fmt.Println("from ch1:", val)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(5 * time.Second):
fmt.Println("timeout!")
default:
fmt.Println("no channel ready")
}
// ── Worker Pool Pattern ──
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
results <- job * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
wg.Wait()
close(results)
for r := range results {
fmt.Println("result:", r)
}
}
// ── Context (cancellation, deadlines, timeouts) ──
func fetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
// Usage with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := fetchWithTimeout(ctx, "https://api.example.com")
// Cancellation via context
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(3 * time.Second)
cancel() // cancel after 3 seconds
}()
select {
case <-ctx.Done():
fmt.Println("cancelled!")
case result := <-ch:
fmt.Println("got result")
}
// ── sync.Mutex ──
var mu sync.Mutex
var muRWM sync.RWMutex
mu.Lock()
// critical section
mu.Unlock()
muRWM.RLock() // multiple readers
// read section
muRWM.RUnlock()
muRWM.Lock() // exclusive writer
// write section
muRWM.Unlock()
// ── sync.Map (concurrent map, lock-free reads) ──
var sm sync.Map
sm.Store("key", "value")
val, ok := sm.Load("key")
sm.Delete("key")
sm.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // continue iteration
})
// ── sync.Once (run function exactly once) ──
var once sync.Once
once.Do(func() {
initialize() // runs only once, even with concurrent calls
})
// ── sync.Pool (object pooling) ──
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
// ... use buf ...
bufPool.Put(buf)| Feature | Goroutine | OS Thread |
|---|---|---|
| Stack size | ~2-8 KB (grows/shrinks) | ~1-8 MB (fixed) |
| Creation cost | ~2 KB | ~1 MB + TLS overhead |
| Scheduling | Go runtime (M:N) | OS kernel |
| Context switch | ~100 ns | ~1000 ns |
| Typical count | 100,000+ | 1,000-10,000 |
| Blocking | Runtime moves goroutine | OS blocks thread |
| Feature | Channel | Mutex |
|---|---|---|
| Communication | Yes (send/receive) | No (shared memory) |
| Synchronization | Yes (implicit) | Yes (explicit lock/unlock) |
| Idiom | "Don't communicate by sharing memory" | When sharing memory is necessary |
| Deadlock risk | Possible (circular waits) | Possible (forgotten unlock) |
| Use case | Pipelines, worker pools, signaling | Counters, caches, state |
// ── Error Interface ──
type error interface {
Error() string
}
// ── Creating Errors ──
err := errors.New("something went wrong")
err := fmt.Errorf("failed to open %s: %w", filename, err) // wrap with context
// ── Custom Error Type ──
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with id=%d not found", e.Resource, e.ID)
}
func (e *NotFoundError) Unwrap() error {
return fmt.Errorf("id: %d", e.ID)
}
// ── Sentinel Errors ──
var ErrNotFound = errors.New("not found")
func FindUser(id int) (*User, error) {
// ...
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
// ── Error Handling Patterns ──
// 1. Check immediately
file, err := os.Open("data.txt")
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer file.Close()
// 2. Helper function that wraps errors
func readFile(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read file %s: %w", path, err)
}
return string(data), nil
}
// 3. Defer close + error check
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
// ... use f ...
return nil
}
// 4. errors.Is / errors.As (unwrap wrapped errors)
func handleErr() {
_, err := os.Open("nonexistent")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file does not exist")
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("path error: %s (op=%s)\n", pathErr.Path, pathErr.Op)
}
}
// 5. Panic / Recover (only for truly unrecoverable errors)
func mustParseInt(s string) int {
v, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("parsing %q: %v", s, err))
}
return v
}
func safeExecute() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
doRiskyThing()
return nil
}
// ── defer order (LIFO — last deferred runs first) ──
func deferred() {
defer fmt.Println("1") // runs third
defer fmt.Println("2") // runs second
defer fmt.Println("3") // runs first
}
// ── panic / recover
// panic stops execution, unwinds stack, calls deferred functions
// recover() catches panic but ONLY inside deferred function
func safeDivide(a, b int) (result int) {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
return a / b // panics if b == 0
}| Feature | error | panic |
|---|---|---|
| Recoverable? | Yes (normal flow) | No (unexpected state) |
| Caller responsibility | Handle with if err != nil | Cannot catch (only defer/recover) |
| Stack unwinding | No | Yes (runs deferred functions) |
| Performance | Fast (no unwinding) | Expensive (unwinds entire stack) |
| When to use | Expected failures (file not found) | Programming bugs (nil pointer) |
| Library code | Always return errors | Never panic |
module github.com/myorg/myproject
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
github.com/go-sql-driver/mysql v1.7.1
github.com/redis/go-redis/v9 v9.4.0
golang.org/x/crypto v0.22.0
)
require (
github.com/stretchr/testify v1.8.4
github.com/testcontainers/testcontainers-go v0.29.0
)
replace github.com/local/pkg => ../local-pkg
retract [v1.0.0, v1.1.0]# ── Go Module Commands ──
go mod init github.com/myorg/myproject # Initialize module
go mod tidy # Download dependencies, clean go.sum
go mod verify # Verify checksums
go mod download github.com/gin-gonic/gin@latest # Pre-download
go mod graph # Dependency graph
go mod edit -require=github.com/foo/bar@v1.2.3 # Edit go.mod
# ── Go Tool Commands ──
go build ./... # Compile all packages
go build -o myapp ./cmd/app # Build with output name
go run ./cmd/app # Build and run
go test ./... # Run all tests
go test ./pkg/mypkg -v # Verbose test output
go test -race ./... # Race detector
go test -cover ./... # Coverage report
go test -coverprofile=cov.out ./... # Coverage profile
go vet ./... # Static analysis
go fmt ./... # Format code
goimports -w . # Organize imports
go doc fmt # Package documentation
# ── Package Structure ──
# myproject/
# cmd/
# myapp/main.go # executable entry point
# internal/
# user/service.go # internal packages (cannot be imported externally)
# pkg/
# utils/helper.go # public library packages
# api/
# handler.go
# go.mod| Element | Convention | Example | ||
|---|---|---|---|---|
| Package | lowercase, no underscore | http | httprouter | |
| File | snake_case | user_service.go | ||
| Variable | camelCase | userName | isActive | |
| Exported | CamelCase (capitalized) | UserService | MaxRetries | |
| Interface | PascalCase + -er suffix | Reader | Writer | Validator |
| Struct | PascalCase | User | Config | HTTPClient |
| Constant | CamelCase or UPPER | MaxRetries | MAX_THREADS | |
| Private | lowercase (unexported) | helperFunc | internalCache |
package user_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/myorg/myproject/internal/user"
)
// ── Basic Test ──
func TestCreateUser(t *testing.T) {
u, err := user.Create("alice", "alice@example.com")
require.NoError(t, err)
assert.Equal(t, "alice", u.Name)
assert.Equal(t, "alice@example.com", u.Email)
assert.NotZero(t, u.ID)
assert.True(t, u.CreatedAt.IsZero() == false)
}
// ── Table-Driven Tests ──
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b, want int
}{
{"positive", 1, 2, 3},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
{"large", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := add(tt.a, tt.b)
if got != tt.want {
t.Errorf("add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
}
})
}
}
// ── Testing Errors ──
func TestInvalidEmail(t *testing.T) {
_, err := user.Create("bob", "invalid-email")
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid email")
}
// ── Mocking with Interfaces ──
type MockStore struct {
users map[string]*user.User
err error
}
func (m *MockStore) Get(id string) (*user.User, error) {
return m.users[id], m.err
}
func TestServiceWithMock(t *testing.T) {
mock := &MockStore{
users: map[string]*user.User{
"1": {ID: "1", Name: "Alice", Email: "a@b.com"},
},
}
svc := user.NewService(mock)
u, err := svc.GetUser("1")
require.NoError(t, err)
assert.Equal(t, "Alice", u.Name)
}
// ── Benchmarks ──
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
add(1, 2)
}
}
func BenchmarkParallelAdd(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
add(1, 2)
}
})
}
// ── TestMain (setup/teardown) ──
func TestMain(m *testing.M) {
// Setup
setupTestDB()
// Run tests
m.Run()
}
// ── Fuzzing (Go 1.18+) ──
func FuzzReverseString(f *testing.F) {
f.Fuzz(func(t *testing.T, s string) {
reversed := reverseString(s)
doubleReverse := reverseString(reversed)
if s != doubleReverse {
t.Errorf("reverse failed: %q -> %q -> %q", s, reversed, doubleReverse)
}
})
}
// ── HTTP Handler Testing ──
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
handler(w, req)
assert.Equal(t, 200, w.Code)
assert.Contains(t, w.Body.String(), "ok")
}| Command | Description |
|---|---|
| go test ./... | Run all tests recursively |
| go test -v ./... | Verbose output |
| go test -run TestName ./... | Run specific test |
| go test -run TestXxx/Yyy | Run subtest |
| go test -race ./... | Race detector |
| go test -cover ./... | Coverage report |
| go test -coverprofile=cov.out | Write coverage profile |
| go test -bench=. ./... | Run benchmarks |
| go test -fuzz=FuzzFunc ./... | Run fuzzer |
| go test -short | Skip long-running tests |
| go test -count=1 -shuffle=on | Randomize test order |
| Function | Failure Behavior |
|---|---|
| assert.Equal | Logs failure, continues test |
| assert.True | Logs failure, continues test |
| assert.NotNil | Logs failure, continues test |
| require.Equal | Logs failure, STOPS test immediately |
| require.NoError | Logs failure, STOPS test immediately |
| require.NotNil | Logs failure, STOPS test immediately |
Use pointers when: you need to modify the original value, share data between goroutines, avoid copying large structs, or when the value might be nil. Use values when: the struct is small (<100 bytes), you're only reading, or you want value semantics (assignment copies). Strings and slices are already reference-like (header with pointer) — no need to add & for them.
Primarily through channels — typed conduits for passing values between goroutines with synchronization guarantees. Go's philosophy: "Don't communicate by sharing memory; share memory by communicating." Alternatives: sync.Mutex for protecting shared memory, sync.WaitGroup for waiting on goroutines, context.Context for cancellation/timeout, and sync.Map/sync.Pool for concurrent data structures.
1) if err != nil — standard immediate check. 2) errors.Is(err, target) — check if error wraps a specific sentinel error. 3) errors.As(err, &typedErr) — unwrap to a specific error type. 4) defer pattern for resource cleanup. 5) errors.Unwrap() — get the wrapped error. Always use fmt.Errorf("context: %w", err) with %w to chain error context.
Go uses a tricolor mark-and-sweep GC: white (not yet examined), grey (in use, not modified since last scan), black (modified). The GC runs concurrently with the program (write barrier) and performs STW (stop-the-world) only for marking roots. It uses a non-generational approach — no separate young/old generations. Since Go 1.8, the GC can reclaim objects with cyclic references. Since Go 1.19, it supports soft memory limits (GOMEMLIMIT).
for range slice iterates with (index, value) — index is integer position. for range map iterates with (key, value) — map iteration order is not guaranteed across runs. For both, you can use _ to ignore one element: for _, v := range slice or for k, _ := range map.
defer schedules a function call to run when the surrounding function returns (LIFO order). Key uses: closing files, releasing locks, undoing database transactions, logging, recovering from panics. Important gotcha: defer captures variables by reference — the value at execution time is used, not declaration time. Avoid deferring in loops (use closures correctly). Each defer has a small overhead (~8 bytes on stack).