Saltar a contenido

12 - Reading Other People's Code

What this session is

About 45 minutes. You'll learn the strategy for reading code you didn't write - a different skill from writing your own. This page has less code than usual; what it teaches is how to approach a new codebase without drowning. Master this and the difference between you and an experienced engineer narrows dramatically.

The mistake most beginners make

When you open a new codebase, the temptation is to start reading the first file you see and try to understand every line. By line 50 you're lost; by line 200 you've given up.

This doesn't work because real code isn't a story - it's a graph. Every function calls others. Every type is defined somewhere else. Trying to load it all into your head at once is impossible, even for experienced engineers.

The trick is to not try. Pick a small thread; follow only it; let the rest stay fuzzy.

The five-minute orientation

Whenever you open a new Go project, do exactly this, in order:

  1. Read the README. What does this project DO? What is the one-sentence elevator pitch? If there's no README or you can't answer this, the project is too unfinished - pick another.

  2. List the top-level directories. Common Go layout:

  3. cmd/ - actual programs you can run (one folder per binary).
  4. internal/ - code that's private to this project.
  5. pkg/ - public reusable packages (less common now; many projects skip this folder).
  6. api/ - schema files (OpenAPI, protobuf, etc.).
  7. docs/ - documentation.
  8. scripts/, hack/, tools/ - helper scripts.
  9. vendor/ - copy of dependencies (rare since Go modules; some projects still use it).

  10. Open go.mod. What's the module path? What are the direct dependencies? This tells you which ecosystem the project lives in.

  11. Find the entry point. Usually main.go or cmd/<name>/main.go. Read main from top to bottom - but don't dive into every function it calls. Get a feeling for the shape: "it parses flags, builds a config, starts a server."

  12. Read one test file. Pick a small _test.go and read it. Tests show you what the code is supposed to do, with concrete examples. Often clearer than the code being tested.

After this five-minute pass, you should be able to write a one-paragraph summary of what the project does. If you can't, repeat.

Tools for reading

A few things make reading 10× faster:

go doc <pkg> - show the documentation for a package, from your terminal.

go doc fmt
go doc fmt.Println
go doc github.com/some/pkg

pkg.go.dev - the web equivalent. Every Go package, indexed and rendered. The first place to look when you encounter an unfamiliar import.

Your editor's "Go to definition" / "Find references." In VS Code, right-click → "Go to Definition" jumps to where a function or type is defined. "Find All References" shows everywhere it's used. This is how you trace a name through a project quickly.

grep -r 'pattern' . - old-school but unbeatable. Find every place a string appears in the codebase.

go test -run TestName -v - run one specific test to watch it. Tests are the most reliable "what does this actually do?" diagnostic.

Reading the diff of a recent PR. GitHub's "Pull requests" tab shows what's changed lately. PRs are bite-sized - a few files, a clear description, a discussion. Often the best way to understand a project is to read its five most recent merged PRs.

A real session (worked example)

Let's read a tiny piece of a real Go project: the standard library's strings.Contains function. We're going to pretend this is a project we just opened.

Step 1: what does it do?

go doc strings.Contains from any terminal:

package strings // import "strings"

func Contains(s, substr string) bool
    Contains reports whether substr is within s.

Clear: takes two strings, returns true if the second appears inside the first.

Step 2: where is it defined?

In your editor, command-click on strings.Contains in any of your own code. Or just look in your Go installation: $(go env GOROOT)/src/strings/strings.go. You'll see:

func Contains(s, substr string) bool {
    return Index(s, substr) >= 0
}

Two lines. It calls Index. So now we need to understand Index.

Step 3: follow one thread.

Go to definition on Index (or scroll up in the same file). Index is a few dozen lines, with some fast-path optimizations. Read the top comment, skim the body, don't try to understand the optimizations. Recognize: "it returns the position of the substring, or -1 if not found." That's enough.

Step 4: confirm with a test.

Open strings_test.go in the same folder. Search for "TestIndex" or "TestContains". Read a few test cases. Now you know - and have verified you know - what these functions do.

Step 5: write the one-line summary.

strings.Contains(s, substr) returns whether substr appears anywhere in s. Implemented in terms of strings.Index, which returns the position or -1.

That whole investigation took ~5 minutes. You did not understand every optimization in Index. That's fine. You understood enough to use it, recognize it in code, and find out more if you need to.

Things you will see that look scary

Real codebases use language features you haven't met yet. A few common ones, with "don't panic" notes:

  • context.Context - passed as the first parameter of most functions in real Go. Used for cancellation and deadlines. For reading: treat it as "the thing that tells me when to stop." We'll meet it in a later path.
  • Generics: func Foo[T any](x T) T - Go added generics in 1.18. [T any] declares a type parameter. Reads as "a function that works on values of any type T." Don't worry about the rules; just read past it.
  • interface{ ... } everywhere - interfaces declare behavior. As mentioned in page 11, a type satisfies an interface by having the right methods, with no explicit declaration. When you see a function take an interface, it works on anything matching.
  • select { case ...: - like switch but for channels. Waits until one of several channel operations is ready. Skip the details on first read; recognize it as "channel multiplexing."
  • Build tags: //go:build linux - at the top of a file, restrict the file to certain platforms. Ignore unless you care about cross-platform builds.
  • Reflection: reflect.Value, reflect.TypeOf - runtime type inspection. Powerful, slow, and used by frameworks (JSON encoding, ORMs). For reading: "this code does something dynamic with types."
  • Generated code - files starting with // Code generated by ... DO NOT EDIT. are produced by a tool. Don't try to understand the implementation; read the intent (the input the generator used) instead.

You will hit things you don't recognize. That's normal even after years. The skill is knowing when to dig in and when to skim past. Most of the time: skim past. Dig in only when the thing matters to whatever you're actually trying to do.

Reading vs. understanding

There's a useful distinction:

  • Reading code means following what it does, line by line. You can read code without understanding it deeply.
  • Understanding code means knowing why it's shaped the way it is. You don't need to understand to contribute.

A first PR to a project often involves reading 1000 lines, understanding 100, modifying 5. That ratio is normal. Don't expect to understand the whole codebase before doing anything in it.

Exercise

No coding this time. Reading.

Pick a small Go project on GitHub. Three suggestions, ordered by smallness:

  • peterbourgon/ff - a small flag library (~2000 LOC). Clean code, well-organized.
  • fatih/color - the colored-printing library you used in page 11. Small, single-purpose.
  • urfave/cli (v2 or v3) - a CLI-application framework. Bigger (~10k LOC) but very well documented.

Pick one. Do the five-minute orientation:

  1. Read the README.
  2. List the top-level directories. What does the layout suggest?
  3. Open go.mod. What does it depend on? (Probably almost nothing, for these.)
  4. Find the entry point. Trace main (or the most-public function) for 5 minutes.
  5. Open the test file for the main code file. Pick three test cases; understand them.

Write a paragraph (in a note file, for yourself) answering: - What does this project do? - How is it organized? - What's the most interesting thing you noticed?

That paragraph is your start point for everything in pages 13-15.

What you might wonder

"What if I don't understand something even after reading it three times?" Write down what you don't understand, skip it, keep going. Come back later. Often the thing that confused you on page 1 makes sense after you've seen page 50. If it still doesn't, ask in the project's discussion forum or issue tracker - but only after you've tried for an hour yourself.

"What about huge projects like Kubernetes?" The same techniques work, just scaled. You won't ever read all of Kubernetes; nobody has. You'll learn one slice of it at a time. The orientation phase becomes "what does this sub-package do?" rather than "what does the whole project do?"

"How do I know which tests are 'representative'?" The ones with the simplest names usually exercise the basic case. TestAddSimple, TestParseEmptyInput, etc. Start there. Save TestRaceConditionUnderContention for later.

Done

You can now: - Apply a five-minute orientation to any new Go project. - Use go doc, pkg.go.dev, and editor navigation to read code efficiently. - Distinguish reading from understanding - and not require the latter to make progress. - Recognize common "looks scary, isn't" patterns: generics, interfaces, context, generated code. - Pick a small project and write a one-paragraph summary of what it does.

The skill on this page is the one separating "people who learned a language" from "people who can contribute to software." Practice it on three projects, not one.

Next page: how to choose a project worth your time, and what makes one "manageable" for a first contribution.

Next: Picking a project13-picking-a-project.md

Comments