Let’s be honest, most of the code you write in your career will be spent reading other people’s. The good news? With Go, you’re often reading the best code—the standard library. It’s our collective textbook, written by masters of the craft. Studying it isn’t just recommended; it’s a shortcut to writing idiomatic Go yourself. Let’s crack it open.

The io.Reader and io.Writer Interfaces: The Universal Adapters

If you learn one thing from the standard library, let it be the power of these two interfaces. They are the duct tape and WD-40 of Go, connecting everything without anyone needing to know what “everything” is.

The beauty is in their stunning simplicity:

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

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

Why is this genius? Because it’s impossible to write a useful Go program without implementing one of these. Reading from a file, a network connection, a string, or compressing a stream—they all look identical to the code consuming them. Your function that logs to an io.Writer doesn’t care if it’s writing to os.Stdout, a file, or a TCP connection. This is dependency injection at its finest, and it’s not some complex framework; it’s just a one-method interface.

Here’s the canonical pattern for reading until an error or EOF (End Of File), which you’ll see everywhere:

func process(r io.Reader) {
    buf := make([]byte, 1024) // Not a bad starting size
    for {
        n, err := r.Read(buf)
        if err == io.EOF {
            break // Gentle, expected closure
        }
        if err != nil {
            log.Fatal(err) // Something actually went wrong
        }
        data := buf[:n] // CRITICAL: You must only use the 'n' bytes read!
        fmt.Print(string(data))
    }
}

Pitfall alert: See that buf[:n]? That’s the most common rookie mistake. The Read method fills the slice up to len(p) but tells you it only put n bytes in there. The rest of the slice is just leftover garbage from the last read. If you use the whole buffer, you’ll be processing that garbage. Don’t do it.

The http.HandlerFunc Type Adapter

This is a masterclass in making interfaces easy to use. The http.Handler interface is straightforward:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

You could create a struct that implements it. But that’s a lot of boilerplate for a simple function. The standard library designers knew this, so they gave us the HandlerFunc type and the corresponding adapter pattern.

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r) // Just call the function itself
}

This is absurdly clever. They defined a type that is a function, and then gave that type a method. This means any function with the signature func(ResponseWriter, *Request) automatically implements http.Handler by virtue of this type conversion. It’s pure magic.

// Your function
func greet(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, world!")
}

// This works because http.HandlerFunc(greet) now has a ServeHTTP method.
http.Handle("/", http.HandlerFunc(greet))

It looks like we’re calling a function called HandlerFunc, but we’re not. We’re converting our greet function into the http.HandlerFunc type. The standard library then calls its ServeHTTP method, which, as we saw, just calls the original greet function. It’s a tiny, elegant bit of indirection that saves us from writing endless structs.

sync.Once: Do It Exactly One Time

Initialization is a common problem. You need something set up, but only once, and it must be thread-safe. You could write a mutex and a boolean flag and check it every time. It’s not hard, but it’s easy to get slightly wrong. The standard library said, “We’ll do that for you,” and gave us sync.Once.

var (
    once     sync.Once
    instance *ExpensiveThing
)

func getInstance() *ExpensiveThing {
    once.Do(func() {
        instance = &ExpensiveThing{
            // ... costly initialization here
        }
    })
    return instance
}

The beauty here is in its absolute guarantee. The Do method’s function will be executed exactly once, no matter how many goroutines call it. The implementation uses atomic memory operations and locking, so you don’t have to. This is the standard library saying, “We’ve handled the tricky concurrency bits. You just focus on what to initialize.” Use it. It’s perfect.

context.Context for Cancellation and Timeouts

The context package is the answer to the messy problem of cancellation and request-scoped values. Its pervasive use in the standard library (especially in net and os packages) tells you everything: this is the idiomatic way to handle timeouts and cancellation.

The key pattern is to always pass the context as the first argument to a function, typically named ctx.

func slowOperation(ctx context.Context) (Result, error) {
    select {
    case <-time.After(10 * time.Second):
        return result, nil // The operation finished
    case <-ctx.Done():
        return nil, ctx.Err() // The context was cancelled or timed out
    }
}

Why is this better than just using a time.Timer? Because it composes. A caller can create a context with a 30-second timeout, and then call ten functions that each need to run with that same overall deadline. If the caller cancels the operation because the user clicked a button, all of those deeply nested functions will get the signal to stop immediately. It creates a clean, unified cancellation tree for your entire call chain. Ignore this pattern at your peril; your future self, debugging a goroutine leak, will thank you for adopting it early.