thompson-unix-philosophy
Ken Thompson Style Guide
Overview
Ken Thompson co-created Unix, the C language, UTF-8, and Go. His approach to software is legendary: build small, sharp tools that do one thing well and compose together. The Unix philosophy is his philosophy.
Core Philosophy
"One of my most productive days was throwing away 1,000 lines of code."
"When in doubt, use brute force."
"I'd rather write programs to write programs than write programs."
Thompson believes in minimalism and pragmatism. Build the simplest thing that works, make it work well, and compose larger systems from small pieces.
Design Principles
-
Do One Thing Well: Each program, function, or module has one job.
-
Compose Small Programs: Build complex behavior from simple pieces.
-
Text Streams as Interface: Universal, simple, debuggable.
-
Brute Force When Appropriate: Don't over-engineer; simple algorithms often win.
When Writing Code
Always
- Make each function do exactly one thing
- Use simple data formats (text, JSON)
- Write programs that can be composed via stdin/stdout
- Start with the simplest solution that could work
- Measure before optimizing
- Make tools that are easy to script
Never
- Build monoliths when pipelines work
- Use complex formats when text suffices
- Optimize without profiling
- Add features "just in case"
- Create interactive tools when batch works
Prefer
- Line-oriented text formats
- Streaming over loading everything into memory
io.Reader/io.Writerfor data flow- Flags over config files for simple tools
- Exit codes for scripting
Code Patterns
Programs as Filters
// Unix philosophy: read stdin, write stdout
func main() {
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
line := scanner.Text()
// Transform
result := process(line)
fmt.Println(result)
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
// Composable: cat file | myprogram | sort | uniq
Do One Thing Well
// BAD: Swiss army knife
func ProcessData(data []byte, format string, compress bool,
encrypt bool, output string) error {
// 200 lines handling all combinations...
}
// GOOD: Separate tools
func Compress(r io.Reader, w io.Writer) error { ... }
func Encrypt(r io.Reader, w io.Writer, key []byte) error { ... }
func Format(r io.Reader, w io.Writer, fmt string) error { ... }
// Compose:
// cat data | compress | encrypt | format > output
Simple Data Flow with io.Reader/Writer
// Everything flows through Reader/Writer
func CountWords(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
scanner.Split(bufio.ScanWords)
count := 0
for scanner.Scan() {
count++
}
return count, scanner.Err()
}
// Works with files
f, _ := os.Open("file.txt")
n, _ := CountWords(f)
// Works with strings
n, _ := CountWords(strings.NewReader("hello world"))
// Works with HTTP responses
resp, _ := http.Get(url)
n, _ := CountWords(resp.Body)
// Works with compressed data
gz, _ := gzip.NewReader(f)
n, _ := CountWords(gz)
Brute Force First
// BAD: Premature optimization
func FindDuplicates(items []string) []string {
// Complex trie-based algorithm with O(n) complexity
// 150 lines of code...
}
// GOOD: Simple and clear (Thompson's way)
func FindDuplicates(items []string) []string {
seen := make(map[string]bool)
var dups []string
for _, item := range items {
if seen[item] {
dups = append(dups, item)
}
seen[item] = true
}
return dups
}
// Profile first. Optimize only if this is actually slow.
Command-Line Tools
package main
import (
"flag"
"fmt"
"os"
)
func main() {
// Simple flags, not complex config
n := flag.Int("n", 10, "number of lines")
flag.Parse()
// Read files from args, or stdin
args := flag.Args()
if len(args) == 0 {
process(os.Stdin, *n)
} else {
for _, filename := range args {
f, err := os.Open(filename)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}
process(f, *n)
f.Close()
}
}
}
// Exit codes matter for scripting
// 0 = success
// 1 = general error
// 2 = usage error
Text as Universal Interface
// BAD: Custom binary format
type Record struct {
// Complex serialization...
}
// GOOD: Line-oriented text (like /etc/passwd)
// name:age:email:role
func ParseRecord(line string) (*Record, error) {
parts := strings.Split(line, ":")
if len(parts) != 4 {
return nil, fmt.Errorf("invalid record: %s", line)
}
age, err := strconv.Atoi(parts[1])
if err != nil {
return nil, err
}
return &Record{
Name: parts[0],
Age: age,
Email: parts[2],
Role: parts[3],
}, nil
}
// Debuggable: you can cat the file
// Composable: grep, awk, sed all work
// Universal: every language can parse it
Mental Model
Thompson asks:
- Can this be simpler? Usually yes.
- Can this be a filter? stdin → stdout
- Does this do one thing? Split it if not.
- Will brute force work? Start there.
The Unix Way in Go
| Unix Tool | Go Equivalent |
|---|---|
cat |
io.Copy(os.Stdout, file) |
head |
bufio.Scanner + counter |
grep |
strings.Contains / regexp |
wc |
bufio.Scanner with splits |
sort |
sort.Strings |
uniq |
map for dedup |
tee |
io.MultiWriter |