TechGuild
1Level 1, Curious Newcomer

Reading Go, and laying out a project

Go has one formatting style, one way to export a name, and a package system where the directory is the unit. This lesson covers the conventions you will be judged by, the project layout a Go reviewer expects, the fmt verbs the code already uses, and what has run before main.

11 min readfoundationFacts checked August 2026
On this page

What you will be able to do

  • Apply Go's naming conventions, including what capitalisation actually controls
  • Lay out a module with cmd, internal and library packages
  • Explain what internal enforces and what it does not
  • Read an unfamiliar Go file top to bottom and know where to look for what
  • Say what runs before main, and why a blank import is not dead code

In Go the directory is the package, and a capital letter is the access control. There is no public, no private, no export clause and no per-file visibility: a name beginning with an upper case letter is reachable from any package that imports yours, and a name that does not begin with one is reachable from every file in the same directory and nowhere else.

Those two facts decide the layout of every Go project you will read.

One formatting style, and no argument about it

Go source is formatted by gofmt, which has no style options, so arguments about brace placement, indentation and import order do not happen in Go. Every file you open has the same shape, and unformatted code is a signal that the author was not running the standard toolchain. The commands and the check that belongs in continuous integration are covered in formatting, vet, modernisers and linters.

Capitalisation is the access control

exported identifier

A top-level name, struct field or method whose first letter is upper case. It is visible to every package that imports the one it is declared in. Anything else is visible only inside its own package, which in Go means its own directory.

In Java, visibility is a keyword attached to a member and the class is the unit of encapsulation, with four levels to choose between. In TypeScript, visibility is an export list and the module, usually one file, is the unit. Go has one bit, carried by the first letter, and the unit is the directory.

The consequence a Java reader misses is that there is no class-private. Every file in a package sees every unexported field and method of every type declared there. Encapsulation in Go is a package boundary or it is nothing, so the way to hide an implementation detail from your own colleagues is to move it into its own package rather than to decorate it with a keyword.

Names get shorter as their scope gets shorter

Go names track the distance between where a value is declared and where it is last used. A receiver is one or two letters, the same letters on every method of that type, and never this or self; a loop index is i, a reader is r, a scratch buffer is buf. An exported package-level identifier is read by strangers, so it gets whole words.

Package names carry the same discipline, and are chosen for how they read at the call site rather than in isolation: lower case, one word, no underscores, singular, so chart.New rather than chart.NewChart and bytes.Buffer rather than bytes.BytesBuffer. A function FormatChartLabel in a package called util reads as util.FormatChartLabel(row) and puts the interesting word last; moved and shortened, the same call is chart.Label(row) and the first six characters name the subsystem. Avoid utils, common, helpers and base, which describe what a package is not.

The directory is the unit, and the layout follows

A module has go.mod at its root, which modules, versions and the toolchain covers in full. Below that, three kinds of directory account for almost every repository worth copying.

app/
  go.mod
  cmd/
    report/
      main.go        // package main, one directory per executable
  internal/
    parse/           // yours alone, enforced by the go command
      parse.go
      parse_test.go
  chart/             // importable by anyone who imports this module
    chart.go

Each executable gets its own directory under cmd/, holding package main and as little else as possible: flag parsing, wiring, and a call into a real package. Everything under internal/ belongs to you. Everything else at the top level is public whether you meant it to be or not. You will also see a pkg/ directory in the wild; it comes from a widely copied template rather than from Go itself, the toolchain attaches no meaning to it, and you do not need it.

What internal enforces, and what it does not

An import path containing internal as a path element may be imported only by code in the tree rooted at that internal directory's parent. The go command checks this while resolving imports, so violating it is a build failure rather than a lint warning that somebody suppresses.

That nesting is the useful part. internal at the module root is importable by your whole repository and nobody else; nested under one command it is scoped to that command. Capitalisation is an independent axis, and the productive combination is both at once: an exported identifier inside an internal package is fully exported, it simply has a smaller audience.

The guarantee is also narrower than it looks. It is not per-file or per-type privacy, and it is not a run time or security control: it stops an import, not a copy of your source. Unlike Java's package-private, which anything declaring the same package name can reach around, and unlike a TypeScript export list, which is erased before anything runs, this is a real compile-time boundary. That is its whole value and also its whole extent.

Reading a Go file top to bottom

Convention rather than the formatter fixes the order of a file, and it is stable enough to navigate by. Imports come in groups separated by blank lines, standard library first, and gofmt sorts within a group without moving anything between groups. Then package-level const and var, then any func init, then types, then the constructor, then methods, then unexported helpers. A doc comment begins with the name of the thing it documents, so it reads as a definition and the identifier is greppable.

// Package chart renders counted results as a fixed-width bar chart.
package chart

import (
	"errors"
	"fmt"
	"io"
	"strings"
)

// ErrEmpty is returned by WriteTo when a Chart has no rows.
var ErrEmpty = errors.New("chart: no rows")

const defaultWidth = 40

// A Chart holds labelled counts. Its zero value is an empty chart ready to use.
type Chart struct {
	width int
	rows  []row
}

type row struct {
	label string
	n     int
}

// New returns a Chart whose longest bar is width columns.
func New(width int) *Chart {
	if width <= 0 {
		width = defaultWidth
	}
	return &Chart{width: width}
}

// Add records one labelled count.
func (c *Chart) Add(label string, n int) {
	c.rows = append(c.rows, row{label: label, n: n})
}

// WriteTo renders c to w and reports how many bytes it wrote.
func (c *Chart) WriteTo(w io.Writer) (int64, error) {
	if len(c.rows) == 0 {
		return 0, ErrEmpty
	}
	var total int64
	for _, r := range c.rows {
		bar := strings.Repeat("#", min(r.n, c.width))
		n, err := fmt.Fprintf(w, "%-12s %s\n", r.label, bar)
		total += int64(n)
		if err != nil {
			return total, fmt.Errorf("write row %q: %w", r.label, err)
		}
	}
	return total, nil
}

Read that file the way a Go reviewer does. Chart, New, Add and WriteTo are the entire public surface, because nothing else starts with a capital, and row will never appear in anyone's editor completions. The receiver is c on every method. And WriteTo returning (int64, error) is not an arbitrary shape: it matches an interface in the standard library, which is the kind of signal a signature carries in Go once you have read a few packages.

The verbs in those format strings

fmt.Fprintf and its relatives take C-style verbs, and this handful covers nearly every line of Go you will read.

VerbPrints
%v %+v %#vthe default form, the same with struct field names, the same as Go syntax
%s %qthe string, and the string double-quoted and escaped
%d %f %tinteger, float, boolean
%Tthe type of the value rather than the value
%wwraps an error, and only inside fmt.Errorf
%-12sany verb takes a width, and - pads on the right

Nothing checks the verb against the operand at compile time: %s on a struct holding an int prints {%!s(int=3)}. Reach for %+v when logging a struct and %q when a value might be empty or carry spaces, because quoting shows you that. Most mismatches are caught by go vet, in formatting, vet, modernisers and linters.

A program's own arguments are os.Args, a []string whose first element is the path the binary was invoked as, so the arguments you meant start at os.Args[1:]. Nothing parses them for you: the standard flag package does, and the loganalyse capstone builds a real command line with it.

What has already run before main

Package-level variables are initialised before any function runs, in dependency order rather than file order, so a var computed from another waits for it. Then every func init() in the package runs, in the order the files reach the compiler, which for the go command is sorted by file name. A package is fully initialised before any package importing it begins, so the graph settles from the leaves up and main runs last. init takes no arguments, returns nothing, cannot be called, and may appear more than once in a file.

That is the machinery behind an import with an underscore in front of it: import _ "github.com/lib/pq" binds no name and exists so that package's init runs, registering something in a table somebody else owns, such as a driver with database/sql, a decoder with image, or net/http/pprof's handlers on the default mux.

What goes inside those files starts with values, constants and the zero value.

Check your understanding

Sign in to take this check

4 questions on this lesson, one at a time, with the reasoning for every option as soon as you answer. Each answer is marked on the server and stored against your account.

An account is free. There is no paid plan, no tier and nothing to buy.