Golang logo

Golang

Organization
MadAppGang
golang

Use when building Go backend services, implementing goroutines/channels, handling errors idiomatically, writing tests with testify, or following Go best practices for APIs/CLI tools.

Overview

PublisherMadAppGang
Repositoryclaude-code
Skill namegolang
Stars
281
Forks
26
Bundled files
Instructions only
LicenseMIT
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by MadAppGang on GitHub. Read the source before you install it.

Installation

Install the Golang AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/MadAppGang/claude-code.git /tmp/claude-code
mkdir -p .claude/skills
cp -r /tmp/claude-code/plugins/dev/skills/backend/golang .claude/skills/golang
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Golang in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Golang on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Golang is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Go Development Best Practices

Version: 2.0.0 Purpose: Comprehensive Go development patterns covering idioms, error handling, concurrency, testing, and quality Scope: Backend development with Go - API services, CLI tools, system software Prerequisites: Basic Go syntax knowledge

Overview

Go (Golang) is designed for simplicity, explicit error handling, and safe concurrent programming. This skill covers production-ready patterns validated by the Go community, official documentation, and industry standards (Uber Engineering, Google).

Core Philosophy:

  • Simplicity: "Clear is better than clever" - favor readable code over abstractions
  • Explicit over implicit: No exceptions, no hidden control flow, visible errors
  • Composition over inheritance: Interfaces and embedding, not class hierarchies
  • Built-in concurrency: Goroutines and channels as first-class primitives
  • Tooling-first: Format, vet, test, and benchmark built into the language

Key Design Principles:

  1. Small interfaces (1-3 methods ideal)
  2. Consumer-side interface placement
  3. Error values, not exceptions
  4. Happy path at left margin
  5. Goroutines must have explicit termination

1. Idiomatic Go Patterns

1.1 Naming Conventions

Package Names:

go
// ✅ GOOD: Package names are single lowercase identifiers
// Import path: "net/url" → package name: url
// Import path: "encoding/json" → package name: json
package url      // from "net/url"
package json     // from "encoding/json"
package strings

// ❌ BAD
package urls            // No plural
package encodingjson    // Don't smash words together
package stringutils     // Too verbose

Getters and Setters:

go
type Account struct {
    balance int
}

// ✅ GOOD: No "Get" prefix
func (a *Account) Balance() int {
    return a.balance
}

func (a *Account) SetBalance(amount int) {
    a.balance = amount
}

// ❌ BAD: Java-style getters
func (a *Account) GetBalance() int {
    return a.balance
}

Error Variables:

go
// Exported sentinel errors (capitalized)
var ErrNotFound = errors.New("not found")
var ErrTimeout = errors.New("timeout")

// Unexported internal errors (lowercase)
var errInternal = errors.New("internal error")

Interface Naming:

go
// ✅ GOOD: Short, descriptive
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// ❌ BAD: Verbose or unclear
type DataReader interface { ... }
type IReader interface { ... }  // No "I" prefix

1.2 Interface Design - "The Bigger the Interface, the Weaker the Abstraction"

Core Principle: Small, consumer-side interfaces provide maximum flexibility.

Single-Method Interfaces (Ideal):

go
// Standard library examples
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Compose interfaces
type ReadCloser interface {
    Reader
    Closer
}

Consumer-Side Interface Placement:

go
// ❌ WRONG: Producer defines interface
package store

type CustomerStorage interface {
    StoreCustomer(Customer) error
    GetCustomer(string) (Customer, error)
    UpdateCustomer(Customer) error
    // 10+ methods...
}

type PostgresStore struct {}
func (s *PostgresStore) StoreCustomer(...) { ... }

// ✅ CORRECT: Consumer defines what it needs
package client

type customerGetter interface {
    GetCustomer(string) (store.Customer, error)
}

func ProcessCustomer(cg customerGetter) {
    customer, _ := cg.GetCustomer("123")
    // Only depends on GetCustomer method
}

Return Concrete Types, Accept Interfaces (Postel's Law):

go
// ✅ GOOD
func NewStore() *PostgresStore {
    return &PostgresStore{}
}

func Process(storage CustomerStorage) error {
    // Accepts interface
}

// ❌ BAD: Returning interface
func NewStore() CustomerStorage {
    return &PostgresStore{}
}

When to Create Interfaces:

  • Multiple implementations exist or are planned
  • Need for testing (mocking dependencies)
  • Decoupling packages
  • NOT for: Single implementation with no testing need

1.3 Happy Path Left, Early Returns

Core Principle: Align success path to left margin, handle errors first.

go
// ❌ BAD: Deep nesting
func join(s1, s2 string, max int) (string, error) {
    if s1 == "" {
        return "", errors.New("s1 is empty")
    } else {
        if s2 == "" {
            return "", errors.New("s2 is empty")
        } else {
            concat, err := concatenate(s1, s2)
            if err != nil {
                return "", err
            } else {
                if len(concat) > max {
                    return concat[:max], nil
                } else {
                    return concat, nil
                }
            }
        }
    }
}

// ✅ GOOD: Happy path aligned left
func join(s1, s2 string, max int) (string, error) {
    if s1 == "" {
        return "", errors.New("s1 is empty")
    }
    if s2 == "" {
        return "", errors.New("s2 is empty")
    }

    concat, err := concatenate(s1, s2)
    if err != nil {
        return "", err
    }

    if len(concat) > max {
        return concat[:max], nil
    }
    return concat, nil
}

Guidelines:

  • Maximum 3-4 levels of nesting
  • Omit else blocks when if returns
  • Handle errors immediately
  • Keep normal flow at lowest indentation

1.4 Composition Over Inheritance

Type Embedding (Struct Composition):

go
// Embedding for method promotion
type Logger struct {
    *log.Logger
    prefix string
}

func NewLogger(prefix string) *Logger {
    return &Logger{
        Logger: log.New(os.Stdout, "", 0),
        prefix: prefix,
    }
}

// Logger methods automatically available
logger := NewLogger("APP")
logger.Println("message") // Calls embedded log.Logger.Println

Interface Composition:

go
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Compose interfaces
type ReadCloser interface {
    Reader
    Closer
}

Warning: Avoid embedding in public APIs:

go
// ❌ BAD: Exposes implementation details
type MyHandler struct {
    http.Handler // Leaks all Handler methods
}

// ✅ GOOD: Explicit delegation
type MyHandler struct {
    handler http.Handler
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Custom logic
    h.handler.ServeHTTP(w, r)
}

1.5 Key Go Idioms

Defer for Cleanup:

go
func processFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // Guaranteed cleanup

    // Multiple returns, all close file
    if condition {
        return nil // File closed
    }

    return process(f) // File closed
}

// Mutex pattern
func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()

    c.value++ // All paths unlock
}

Critical Rule: Call defer AFTER checking error:

go
// ❌ WRONG
defer f.Close() // f is nil if Open failed
f, err := os.Open(path)

// ✅ CORRECT
f, err := os.Open(path)
if err != nil {
    return err
}
defer f.Close()

Multiple Return Values:

go
// (value, error) - Standard error handling
func GetUser(id string) (*User, error) {
    // ...
}

// (value, bool) - "comma ok" idiom
value, ok := myMap[key]
if !ok {
    // key not found
}

result, ok := someValue.(TargetType)
if !ok {
    // type assertion failed
}

data, ok := <-channel
if !ok {
    // channel closed
}

Blank Identifier _:

go
// Ignore unwanted values
_, err := os.Open(filename)

// Compile-time interface check
var _ http.Handler = (*MyHandler)(nil)

// Import for side effects
import _ "net/http/pprof"

Useful Zero Values:

go
// sync.Mutex - ready to use
var mu sync.Mutex
mu.Lock() // Works immediately

// bytes.Buffer - valid empty buffer
var buf bytes.Buffer
buf.WriteString("hello") // No initialization needed

// Slices - safe to read
var s []int
fmt.Println(len(s)) // 0 (safe)

2. Error Handling

2.1 Error Wrapping with %w (Go 1.13+)

Core Pattern: Wrap errors with context using fmt.Errorf and %w.

go
func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        // Wrap with context using %w
        return fmt.Errorf("failed to open file %s: %w", path, err)
    }
    defer file.Close()

    data, err := io.ReadAll(file)
    if err != nil {
        return fmt.Errorf("failed to read file %s: %w", path, err)
    }

    return processData(data)
}

// Result when error bubbles up:
// "failed to initialize: failed to open file config.json: open config.json: no such file or directory"

Checking Wrapped Errors:

go
// errors.Is - Check for specific error in chain
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File doesn't exist")
}

// errors.As - Extract specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Printf("Path error on: %s\n", pathErr.Path)
}

Critical: Use %w, NOT %v:

go
// ❌ WRONG: Breaks error chain
return fmt.Errorf("failed: %v", err)

// ✅ CORRECT: Preserves chain
return fmt.Errorf("failed: %w", err)

2.2 Sentinel Errors vs Custom Error Types

Sentinel Errors (Package-Level Variables):

go
package db

var (
    ErrConnectionFailed = errors.New("database connection failed")
    ErrRecordNotFound   = errors.New("record not found")
    ErrDuplicateKey     = errors.New("duplicate key violation")
)

func GetUser(id int) (*User, error) {
    // ...
    if notFound {
        return nil, ErrRecordNotFound
    }
    return user, nil
}

// Caller checks with errors.Is
user, err := db.GetUser(123)
if errors.Is(err, db.ErrRecordNotFound) {
    // Handle not found
}

Custom Error Types (Rich Context):

go
type ValidationError struct {
    Field   string
    Value   interface{}
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed for field '%s': %s (value: %v)",
        e.Field, e.Message, e.Value)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Value:   age,
            Message: "must be non-negative",
        }
    }
    return nil
}

// Caller extracts rich information
if err := validateAge(-5); err != nil {
    var valErr *ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("Field: %s, Value: %v\n", valErr.Field, valErr.Value)
    }
}

Decision Guide:

  • Sentinel errors: Simple, global error conditions
  • Custom types: Errors needing structured data or methods

Important: Use pointer receivers for error types:

go
// ✅ CORRECT: Pointer receiver
func (e *ValidationError) Error() string { ... }

// ❌ WRONG: Value receiver (breaks errors.As)
func (e ValidationError) Error() string { ... }

2.3 Handle Errors Once

Core Principle: Either log the error OR return it, not both.

go
// ❌ BAD: Handle twice (log AND return)
if err != nil {
    log.Printf("error: %v", err)  // Logged here
    return err                     // And returned
}

// ✅ GOOD: Return error, let caller handle
if err != nil {
    return fmt.Errorf("process: %w", err)
}

// ✅ GOOD: Log and handle completely
if err != nil {
    log.Printf("non-fatal error: %v", err)
    // Continue execution (error handled)
}

Error Message Conventions:

go
// ✅ GOOD
var ErrNotFound = errors.New("configuration file not found")
return fmt.Errorf("failed to read settings for user %d: %w", userID, err)

// ❌ BAD
var ErrNotFound = errors.New("Error: Configuration file not found.") // No prefix, no punctuation
return fmt.Errorf("Error occurred: %v", err) // Too generic

2.4 Panic vs Error Decision Tree

Is this condition expected during normal operation?
├─ Yes → Return error
└─ No → Is this a programmer error?
    ├─ Yes → Panic (with clear message)
    └─ No → Is the program in an invalid state?
        ├─ Yes → Panic
        └─ No → Return error

Use Errors When:

go
// Expected failures
func readConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read config: %w", err)
    }
    return parseConfig(data)
}

// Business logic failures
func createUser(email string) error {
    if !isValidEmail(email) {
        return fmt.Errorf("invalid email format: %s", email)
    }
    return nil
}

Use Panic When:

go
// Nil argument (programmer error, document this!)
func ProcessData(data *Data) {
    if data == nil {
        panic("ProcessData: data argument must not be nil")
    }
    // ...
}

// Initialization failure
func init() {
    cfg, err := loadConfig()
    if err != nil {
        panic(fmt.Sprintf("fatal: failed to load config: %v", err))
    }
    globalConfig = cfg
}

// Impossible condition (indicates bug)
func (sm *StateMachine) transition(event Event) {
    newState := sm.computeNextState(event)
    if !sm.isValidTransition(newState) {
        panic(fmt.Sprintf("BUG: invalid state transition from %v to %v",
            sm.currentState, newState))
    }
    sm.currentState = newState
}

Recovery (Use Sparingly):

go
// HTTP server recovering from handler panics
func safeHandler(h http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("Handler panic: %v\n%s", rec, debug.Stack())
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        h(w, r)
    }
}

2.5 Concurrent Error Handling

Pattern 1: errgroup (Coordinated Goroutines):

go
import "golang.org/x/sync/errgroup"

func processFiles(ctx context.Context, files []string) error {
    g, ctx := errgroup.WithContext(ctx)

    for _, file := range files {
        file := file // Capture loop variable
        g.Go(func() error {
            return processFile(ctx, file)
        })
    }

    // Wait for all, return first error
    if err := g.Wait(); err != nil {
        return fmt.Errorf("file processing failed: %w", err)
    }
    return nil
}

Pattern 2: Error Channel (Collect All Errors):

go
func processAll(items []Item) []error {
    errChan := make(chan error, len(items))
    var wg sync.WaitGroup

    for _, item := range items {
        wg.Add(1)
        go func(i Item) {
            defer wg.Done()
            if err := process(i); err != nil {
                errChan <- err
            }
        }(item)
    }

    go func() {
        wg.Wait()
        close(errChan)
    }()

    var errs []error
    for err := range errChan {
        errs = append(errs, err)
    }
    return errs
}

3. Concurrency Patterns

3.1 Goroutine Lifecycle and Leak Prevention

Core Principle: Every goroutine must have an explicit termination mechanism.

Pattern: Context Cancellation + WaitGroup:

go
func runWorkers(ctx context.Context, n int) {
    var wg sync.WaitGroup

    for i := 0; i < n; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            for {
                select {
                case <-ctx.Done():
                    return // Clean exit
                default:
                    doWork(id)
                }
            }
        }(i)
    }

    wg.Wait() // Wait for all goroutines
}

// Usage
ctx, cancel := context.WithCancel(context.Background())
go runWorkers(ctx, 10)

// Later: stop all workers
cancel()

Common Leak: Unbuffered Channel Send:

go
// ❌ LEAK: Goroutine blocks forever if no receiver
func leak() {
    ch := make(chan int)
    go func() {
        ch <- 42 // Blocks forever
    }()
    // Function returns, goroutine leaked
}

// ✅ FIX: Buffered channel or ensure receiver
func fixed() {
    ch := make(chan int, 1) // Buffer size 1
    go func() {
        ch <- 42 // Won't block
    }()
}

3.2 Channel Patterns

Unbuffered vs Buffered Semantics:

go
// Unbuffered: Synchronous handoff
done := make(chan bool)
go func() {
    doWork()
    done <- true // Blocks until main receives
}()
<-done // Guaranteed: work completed

// Buffered: Asynchronous
jobs := make(chan Job, 100)
for w := 0; w < numWorkers; w++ {
    go func() {
        for job := range jobs {
            process(job)
        }
    }()
}

Channel Closing Rules:

go
// ✅ GOOD: Only sender closes
jobs := make(chan Job)
go func() {
    for _, job := range allJobs {
        jobs <- job
    }
    close(jobs) // Signal: no more jobs
}()

for job := range jobs {
    process(job) // Exits when channel closed
}

// ❌ NEVER: Close from receiver
// ❌ NEVER: Close closed channel (panics)
// ❌ NEVER: Send on closed channel (panics)

Select Pattern for Cancellation:

go
func worker(ctx context.Context, jobs <-chan Job) {
    for {
        select {
        case job := <-jobs:
            process(job)
        case <-ctx.Done():
            return // Cancel signal
        }
    }
}

3.3 Context Package for Cancellation

Context Types:

go
// Root contexts
ctx := context.Background() // Main/init
ctx := context.TODO()       // Placeholder

// Cancellation
ctx, cancel := context.WithCancel(parent)
defer cancel() // Always call

// Timeout
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
defer cancel()

// Deadline
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
defer cancel()

// Values (use sparingly, only for request-scoped data)
ctx = context.WithValue(parent, key, value)

Best Practices:

go
// ✅ GOOD: Context as first parameter
func makeRequest(ctx context.Context, url string) error {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := client.Do(req)
    if err != nil {
        return err // Returns context.DeadlineExceeded on timeout
    }
    defer resp.Body.Close()
    return nil
}

// Check cancellation in loops
func longRunning(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            processChunk()
        }
    }
}

Context Rules:

  1. Pass context as first parameter: func Do(ctx context.Context, ...)
  2. Never store context in struct
  3. Always call cancel function (prevents leak)
  4. Use WithValue only for request-scoped data, not options

3.4 Sync Primitives

sync.Mutex:

go
type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

sync.RWMutex (Read-Heavy Workloads):

go
type Cache struct {
    mu    sync.RWMutex
    items map[string]Item
}

func (c *Cache) Get(key string) (Item, bool) {
    c.mu.RLock() // Multiple readers allowed
    defer c.mu.RUnlock()
    item, ok := c.items[key]
    return item, ok
}

func (c *Cache) Set(key string, item Item) {
    c.mu.Lock() // Exclusive write
    defer c.mu.Unlock()
    c.items[key] = item
}

sync.WaitGroup:

go
var wg sync.WaitGroup

for _, item := range items {
    wg.Add(1) // BEFORE starting goroutine
    go func(i Item) {
        defer wg.Done()
        process(i)
    }(item)
}

wg.Wait() // Block until all complete

sync.Once (One-Time Initialization):

go
var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
        instance.init()
    })
    return instance
}

sync/atomic (Lock-Free):

go
type Counter struct {
    value atomic.Int64 // Go 1.19+
}

func (c *Counter) Increment() int64 {
    return c.value.Add(1)
}

When to Use What:

  • Mutex: Protecting compound operations, complex state
  • RWMutex: Read-heavy (10:1 read:write ratio+)
  • WaitGroup: Waiting for goroutines
  • Once: Lazy initialization
  • Atomic: Simple counters, flags
  • Channels: Communication, coordination

3.5 Worker Pool Pattern

go
func workerPool(ctx context.Context, numWorkers int, jobs <-chan Job, results chan<- Result) {
    var wg sync.WaitGroup

    for w := 0; w < numWorkers; w++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for {
                select {
                case job, ok := <-jobs:
                    if !ok {
                        return // Jobs channel closed
                    }
                    result := processJob(job)
                    select {
                    case results <- result:
                    case <-ctx.Done():
                        return
                    }
                case <-ctx.Done():
                    return
                }
            }
        }(w)
    }

    wg.Wait()
    close(results) // Signal completion
}

// Usage
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

jobs := make(chan Job, 100)
results := make(chan Result, 100)

go workerPool(ctx, 10, jobs, results)

// Send jobs
go func() {
    for _, job := range allJobs {
        jobs <- job
    }
    close(jobs)
}()

// Collect results
for result := range results {
    handleResult(result)
}

3.6 Race Detection

Running Race Detector:

bash
go test -race ./...
go build -race
go run -race main.go

Common Race Conditions:

go
// ❌ RACE: Unsynchronized map
var cache = make(map[string]string)

func get(key string) string {
    return cache[key] // RACE
}

func set(key, value string) {
    cache[key] = value // RACE
}

// ✅ FIX: Use sync.Map
var cache sync.Map

func get(key string) string {
    val, _ := cache.Load(key)
    return val.(string)
}

// ❌ RACE: Loop variable capture
for _, item := range items {
    go func() {
        process(item) // RACE
    }()
}

// ✅ FIX: Pass as parameter
for _, item := range items {
    go func(i Item) {
        process(i)
    }(item)
}

4. Testing Patterns

4.1 Table-Driven Tests

Standard Pattern:

go
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"mixed signs", -2, 3, 1},
        {"zeros", 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)
            }
        })
    }
}

Best Practices:

  • Always use t.Run() for subtests
  • Descriptive test case names
  • Use anonymous structs for test data
  • Enable parallel execution with t.Parallel()

4.2 Test Helpers with t.Helper()

go
func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper() // Error points to caller, not here
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func setupTestDB(t *testing.T) *sql.DB {
    t.Helper()
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatalf("failed to open test db: %v", err)
    }

    t.Cleanup(func() {
        db.Close() // Automatic cleanup
    })

    return db
}

4.3 Integration vs Unit Testing

Unit Test (Fast, Isolated):

go
func TestCalculatePrice(t *testing.T) {
    t.Parallel()

    tests := []struct {
        name     string
        quantity int
        price    float64
        expected float64
    }{
        {"single item", 1, 10.0, 10.0},
        {"multiple items", 5, 10.0, 50.0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := CalculatePrice(tt.quantity, tt.price)
            if result != tt.expected {
                t.Errorf("got %v, want %v", result, tt.expected)
            }
        })
    }
}

Integration Test (Build Tag):

go
//go:build integration
// +build integration

package myapp_test

func TestDatabaseOperations(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }

    db := setupTestDatabase(t)
    defer db.Close()

    err := InsertUser(db, &User{Name: "John"})
    if err != nil {
        t.Fatalf("failed to insert user: %v", err)
    }
}

Running Tests:

bash
go test ./...                      # Unit tests only
go test -short ./...               # Skip slow tests
go test -tags=integration ./...    # Integration tests

5. Quality Checks

5.1 golangci-lint Configuration

Recommended .golangci.yml:

yaml
run:
  timeout: 5m

linters:
  enable:
    - errcheck      # Unchecked errors
    - gosimple      # Simplify code
    - govet         # Go vet
    - staticcheck   # Static analysis
    - unused        # Unused code
    - gofmt         # Formatting
    - goimports     # Imports
    - revive        # Fast linter
    - gosec         # Security
    - errorlint     # Error wrapping

linters-settings:
  errcheck:
    check-type-assertions: true
    check-blank: true

  govet:
    enable-all: true

  revive:
    rules:
      - name: error-strings
      - name: error-naming
      - name: exported
      - name: indent-error-flow

issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - errcheck
        - gosec

5.2 Running Quality Checks

Standard Workflow:

bash
# Format Go code
go fmt ./...

# Static analysis
go vet ./...

# Comprehensive linting (if golangci-lint installed)
golangci-lint run

# Run tests with race detector
go test -race ./...

# Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

CI/CD Integration (GitHub Actions):

yaml
- name: golangci-lint
  uses: golangci/golangci-lint-action@v3
  with:
    version: latest

- name: Tests
  run: go test -race -coverprofile=coverage.out ./...

- name: Coverage
  run: |
    COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}')
    if (( $(echo "$COVERAGE < 80" | bc -l) )); then
      exit 1
    fi

6. Structured Logging Patterns

6.1 Structured Logging with slog (Go 1.21+)

Basic Usage:

go
import "log/slog"

func main() {
    // JSON handler for production
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    logger.Info("server starting",
        slog.String("port", "8080"),
        slog.Int("workers", 10))

    // With context
    logger.InfoContext(ctx, "request processed",
        slog.String("method", "GET"),
        slog.String("path", "/api/users"),
        slog.Duration("latency", 45*time.Millisecond))
}

Log Levels:

go
logger.Debug("debug message")    // Development
logger.Info("info message")      // General info
logger.Warn("warning message")   // Warnings
logger.Error("error message")    // Errors

Request Context Logging:

go
func requestLogger(ctx context.Context, logger *slog.Logger) *slog.Logger {
    requestID := ctx.Value("request_id").(string)
    return logger.With(
        slog.String("request_id", requestID),
        slog.String("user_id", getUserID(ctx)),
    )
}

// Usage in handler
func handleRequest(w http.ResponseWriter, r *http.Request) {
    log := requestLogger(r.Context(), baseLogger)

    log.Info("processing request",
        slog.String("path", r.URL.Path),
        slog.String("method", r.Method))

    // All logs include request_id and user_id
    log.Error("database query failed",
        slog.String("error", err.Error()))
}

7. Anti-Patterns to Avoid

Critical Anti-Patterns with Severity Tags

1. [CRITICAL] Swallowing Errors:

go
// ❌ WRONG
data, _ := fetchData()

// ✅ CORRECT
data, err := fetchData()
if err != nil {
    return fmt.Errorf("fetch failed: %w", err)
}

2. [CRITICAL] Using %v Instead of %w:

go
// ❌ WRONG: Breaks error chain
return fmt.Errorf("failed: %v", err)

// ✅ CORRECT
return fmt.Errorf("failed: %w", err)

3. [HIGH] Defer in Hot Loops:

go
// ❌ WRONG: Defers accumulate
for _, item := range items {
    mu.Lock()
    defer mu.Unlock() // Never executes until function returns
    process(item)
}

// ✅ CORRECT
for _, item := range items {
    mu.Lock()
    process(item)
    mu.Unlock()
}

4. [CRITICAL] Goroutine Leaks:

go
// ❌ WRONG: No way to stop
go func() {
    for {
        doWork()
    }
}()

// ✅ CORRECT: Context cancellation
go func() {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            doWork()
        }
    }
}()

5. [CRITICAL] Loop Variable Capture:

go
// ❌ WRONG: All goroutines see last value
for _, item := range items {
    go func() {
        process(item) // RACE
    }()
}

// ✅ CORRECT: Pass as parameter
for _, item := range items {
    go func(i Item) {
        process(i)
    }(item)
}

6. [MEDIUM] Map Without Pre-allocation:

go
// ❌ WRONG: Multiple rehashes
m := make(map[string]Item)
for _, item := range items {
    m[item.ID] = item
}

// ✅ CORRECT: Pre-sized
m := make(map[string]Item, len(items))

7. [HIGH] time.After in Loops (Memory Leak):

go
// ❌ WRONG: Creates timer each iteration
for {
    select {
    case <-time.After(5 * time.Second):
        timeout()
    }
}

// ✅ CORRECT: Reuse timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
    select {
    case <-timer.C:
        timeout()
        timer.Reset(5 * time.Second)
    }
}

8. [HIGH] Checking Error Strings:

go
// ❌ WRONG: Fragile
if err != nil && strings.Contains(err.Error(), "not found") {
    // ...
}

// ✅ CORRECT: Use errors.Is
if errors.Is(err, sql.ErrNoRows) {
    // ...
}

9. [CRITICAL] Not Calling cancel():

go
// ❌ WRONG: Context leak
ctx, cancel := context.WithCancel(parent)
doWork(ctx)

// ✅ CORRECT: Always defer cancel
ctx, cancel := context.WithCancel(parent)
defer cancel()
doWork(ctx)

10. [MEDIUM] Producer-Side Interfaces:

go
// ❌ WRONG
package store
type Storage interface { ... }
type Store struct {}

// ✅ CORRECT
package client
type storage interface { ... } // Define where used

References

Official Documentation:

Industry Standards:

Research Source:

  • Comprehensive research: /ai-docs/sessions/dev-research-golang-best-practices-20260106-233135-773b0173/report.md
  • 15 unanimous consensus patterns
  • 42 high-quality sources (100% official/industry standards)
  • Zero contradictions found

Skill Version: 2.0.0 Last Updated: January 7, 2026 Maintainer: MadAppGang/claude-code

Frequently asked questions

What does the Golang AI skill do?

Use when building Go backend services, implementing goroutines/channels, handling errors idiomatically, writing tests with testify, or following Go best practices for APIs/CLI tools.

Why use Golang on TypingMind?

Because you install it once and use it with any model. Golang is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Golang in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/MadAppGang/claude-code/tree/main/plugins/dev/skills/backend/golang. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Golang?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Golang?

As many as you like. As long as a model supports skills, you can use Golang with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Golang AI skill free?

Yes. It is published on GitHub by MadAppGang under the MIT license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇