Go

Panic Is Not an os.Error

Go has both ordinary failure values and panic, which guarantees that programmers will eventually use one where the other belongs. I started by treating panic like an exception mechanism for file failures. That made simple callers mysterious and was the wrong model.

An operation expected to fail should return an os.Error in this snapshot:

func load(name string) ([]byte, os.Error) {
    f, err := os.Open(name, os.O_RDONLY, 0)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    return readAll(f)
}

Missing files, refused connections, and malformed input are normal outcomes at a system boundary. The caller needs to decide what to do, so the value belongs in the function’s contract.

Panic is better reserved for a broken invariant or a condition from which the current operation cannot sensibly continue. During panic, normal execution stops and deferred calls run while the stack unwinds. If nothing recovers, the program terminates with diagnostic information.

recover can intercept that process, but only when called from a deferred function during unwinding. Calling it during ordinary execution does nothing useful. My worker boundary uses the pattern to prevent one internal invariant failure from taking down the entire test harness:

func runJob(job Job, result chan<- Result) {
    defer func() {
        if value := recover(); value != nil {
            result <- failedResult(value)
        }
    }()
    result <- execute(job)
}

This boundary records the failure and marks that job bad. It does not silently resume halfway through execute; its state may be inconsistent. Recovery belongs at a boundary that can abandon the failed unit of work.

I avoid broad recovery in library functions. Converting every panic into os.Error can hide programmer defects, and recovering without reporting the value destroys the most useful evidence. A panic due to an indexing mistake is not suddenly a network timeout because both traveled through one handler.

These names and details reflect the April 2011 toolchain. Panic and recovery semantics have changed during Go’s development, so old examples need their snapshot attached. The design rule is stable enough for me: return ordinary failures, panic on violated assumptions sparingly, and recover only where an entire operation can be discarded cleanly. Anything broader starts to resemble sweeping broken glass under a very concurrent rug.

Cache Lines Beat Clever Loops

I translated a matrix filter from C to Go and then spent an afternoon optimizing arithmetic that was not the main cost. The profile pointed at memory movement, so I reduced the problem to traversal order.

The matrix is stored in one slice in row-major order. These loops calculate the same sum:

for y := 0; y < height; y++ {
    for x := 0; x < width; x++ {
        sum += pixels[y*width+x]
    }
}
for x := 0; x < width; x++ {
    for y := 0; y < height; y++ {
        sum += pixels[y*width+x]
    }
}

The first walks adjacent elements. The second jumps by an entire row. On a large image the row-wise loop is much faster because each fetched cache line supplies several values used immediately. The column-wise loop asks the processor to fetch many lines and uses a small part of each before moving on.

Bounds checks and multiplication were my original suspects. Hoisting the row offset made the code clearer and helped a little:

for y := 0; y < height; y++ {
    row := pixels[y*width:(y+1)*width]
    for x := 0; x < width; x++ {
        sum += row[x]
    }
}

But traversal order remained the large effect. A more sophisticated arithmetic trick could not compensate for unfriendly access.

I benchmark with enough data to exceed the small caches, repeat the operation, and verify the sum so the compiler cannot discard the work. These are 2011 compiler snapshots, so absolute timings and optimization behavior are temporary facts. The memory hierarchy is less temporary.

I alternate test order as well. Running one version first every time can give it consistently colder data and turn benchmark order into a hidden input.

This also affects data structure choice. A slice of small structures may have better locality than a slice of pointers to separately allocated structures. The latter adds pointer chasing and gives the allocator freedom to scatter objects. It may still be right when identity and independent lifetimes matter, but it is not free.

My practical conclusion is embarrassingly physical: visit data in the order it lies in memory. Before unrolling loops or translating a bit trick from C, inspect the access pattern. The processor is very fast when fed and becomes an expensive space heater when asked to wait.

Replacing Signals With Small Interfaces

I ported the non-visual half of a Qt utility to Go and immediately missed signals and slots. They had been carrying progress, log messages, and completion notifications between objects. My first replacement was a channel for every event. It worked and looked like a telephone exchange designed by an enthusiastic octopus.

The simpler answer for synchronous notifications was a small interface:

type Progress interface {
    Step(done, total int)
}

func scan(files []string, progress Progress) os.Error {
    for i, name := range files {
        if err := scanFile(name); err != nil {
            return err
        }
        progress.Step(i+1, len(files))
    }
    return nil
}

The terminal implementation prints a line. A test implementation records calls. A future graphical shell can arrange delivery to its UI thread. The scanner knows only the behavior it needs.

This differs from a Qt signal in an important way: the call is ordinary and synchronous. The scanner waits while Step runs. That makes control flow and failure behavior obvious, but a slow observer slows the operation. If isolation is required, an adapter can implement Progress by sending values to a channel consumed elsewhere.

I prefer putting that asynchronous decision in the adapter rather than in the core interface. A channel everywhere turns every notification into a lifetime and shutdown problem. An interface everywhere can accidentally block. Neither mechanism removes design; they merely make different design visible.

For optional progress I use a no-op implementation rather than scattering nil checks through the scanner. It is one tiny type with one empty method, and it keeps the hot path unsurprising.

Completion remains a return, not another notification. The caller already has a direct control path, and duplicating it creates two accounts of whether work finished.

The Go compiler decides satisfaction structurally. My terminal reporter does not inherit from a framework base or declare that it implements Progress; matching Step is enough. That made extracting this contract from existing code much easier than I expected.

This is written against an early 2011 weekly snapshot, including the os.Error return convention. APIs will move. The lesson I am keeping is narrower: use a direct small interface for required behavior, then add a channel at the boundary where asynchronous delivery is actually needed. Not every callback needs its own postal service.

Merging Sorted Streams With Channels

I had several sorted result files and wanted one sorted stream without loading everything into memory. The classic algorithm is a k-way merge: keep the next item from each input, emit the smallest, then advance only that input.

My first Go version gave every file reader a goroutine and a channel. The merger holds one current value per channel:

type item struct {
    value int
    open  bool
}

func source(values []int) <-chan int {
    out := make(chan int)
    go func() {
        for _, value := range values {
            out <- value
        }
        close(out)
    }()
    return out
}

The merge loop initializes one item from every source, scans those current values for the smallest, emits it, and receives the replacement from the chosen source. When that receive reports the channel closed, the source leaves consideration. Memory remains proportional to the number of sources, apart from buffering in the file readers.

A linear scan makes each output operation O(k)O(k), where kk is the number of inputs. That is fine for four files and silly for four thousand. Replacing the scan with a binary heap makes selection O(logk)O(\log k). I implemented both and found the simple scan faster for my actual case of eight inputs. Algorithms still have constants, despite textbooks’ efforts to protect us from them.

Channels help isolate blocking input, but they do not improve the comparison algorithm. That distinction matters. Concurrency lets readers overlap waiting on disks or pipes; the heap changes CPU work. Combining both without measuring can add scheduling overhead to a problem that was already fast.

There is also a shutdown question. If the consumer stops early, source goroutines may remain blocked trying to send. My real version includes a separate stop channel selected by each producer, and the caller closes it when abandoning the merge. A finite example that always drains output can omit this, but reusable code cannot.

This code targets the January 2011 snapshot. Channel receive conventions and library calls should be checked against that compiler. The useful design survives those changes: one lookahead value per sorted source, explicit ownership of advancement, and a selection structure chosen for the actual number of streams.

One Thread, Two Threads

I ran two independent CPU loops in goroutines and expected my dual-core machine to halve the time. It did not. Expectations remain the cheapest profiler available and the least accurate.

This runtime defaults to limited processor use. Setting GOMAXPROCS=2 for the experiment allowed Go work to execute on two operating-system threads:

$ time GOMAXPROCS=1 ./spin
real  0m3.84s

$ time GOMAXPROCS=2 ./spin
real  0m2.17s

The result is not exactly half. Both loops touch memory, the scheduler has work, and the rest of the machine has declined to disappear for my benchmark.

Goroutines and parallel execution are separate ideas. Thousands of goroutines can make a concurrent design clear while one thread runs them. Increasing the thread allowance can provide parallelism when there is independent CPU work and hardware to execute it.

The early scheduler also has rough edges. A tight loop with no calls may delay other goroutines more than I expect from mature thread schedulers. I add real synchronization rather than relying on hopeful fairness, and I test the weekly runtime I intend to use.

For I/O-heavy programs, two processor threads may change little. For CPU loops, it can help until memory bandwidth or coordination dominates. The variable is a knob, not a turbo button. Turbo buttons at least had a satisfying light.