---
title: "Go's Design Goals"
source: 'https://academia.sh/en/courses/go-fundamentals/design-goals'
course: 'Go Fundamentals'
language: en
updated: '2026-09-13T05:08:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Classifies whether a source compiles by calling a compiler from inside the program itself'
    - 'Runs the four things the compiler rejects side by side in the same frame'
    - 'Measures that a rejection does not shorten the source but instead lengthens it'
---

# Go's Design Goals

Four things the Go compiler rejects — an unused variable, an unused import, an unwritten return value, an implicit type conversion — are run from inside the program by calling a compiler; all four stop compilation, and rejecting the implicit conversion grows the source doing the same work from 15 characters to 22.

M08's six courses asked the same platform six separate questions; the last one measured a
behavior that in the source might trigger from no call at all — a provider present on the
class path could activate without a single line written in the source. This course walks
through the same door but moves the question from the platform to **the language itself**: a
language decides for itself what stands in for the thing you did not write. In some places
that stand-in is a defined value; in others the language refuses to recognize the unwritten
state at all and rejects the source outright.

This first lesson measures rejection. The Go compiler refuses to accept four forms that would
never cause a problem at runtime: an unused variable, an unused import, an unwritten return
value, an implicit type conversion. All four are, in most other languages, mostly a warning;
here the source never turns into a class file or an executable at all. The question asked is:
when one of these is left unwritten, what does the compiler do, and is this rejection really a
restriction, or does it just move what needed to be written somewhere else?

The rest of the course will keep asking the same triple question: does an unwritten thing find
a **defined** counterpart in the language, is it **rejected** by the compiler, or is the
outcome left to runtime and **open**? This first lesson takes up only the middle of the three
answers — rejection — because rejection draws the sharpest line around a language's
boundaries: when a language does not allow something, there is no need to even discuss how
that thing would have behaved.

## The Chain and This Lesson's Share

The Programming Foundations course (M01/K02) built variable definition, operators, and
explicit versus implicit conversion using Python; there, implicit conversion was mostly a
runtime behavior that passed without incident. **That concept is not rebuilt here** — one
sentence is enough to state the difference: in Go the same implicitness is not a runtime event
but a **compile-time rejection**. The How Computers Work course (M01/K01) built the
compiler–interpreter distinction as a concept; in this lesson the compiler is not a concept but
**a tool called from inside a program**, and the only answer it gives is whether the source
passed or not.

The Data Structures course (M01/K03) built the array, the list, and the map as abstract data
types and compared them by complexity; this curriculum does not repeat that comparison. M09's
share is **which mechanism in Go** realizes those already-built concepts — in this first
lesson the mechanism is not yet a data type, it is the stance the compiler takes toward the
source. The lessons that follow will carry this mechanism into variables, types, and control
flow in turn.

## Three Design Goals, One Measurable Outcome

Three of Go's design goals are cited most often: simplicity, compile speed, readability. This
lesson does not run all three directly — duration is not a unit of measurement, because how
long a compilation takes depends on the machine and the load at that moment and varies from
one run to the next. But there is one place where all three leave a **visible trace** in the
source: what the compiler rejects. If a language accepts every construct, the reader has to
infer from the source which path was intended; if a language rejects a construct, the party
writing it is forced to **carry** their intent into the source. Simplicity here is not "writing
fewer characters" but the smallness of the number of forms the language accepts: the more ways
there are to do one task, the more ways the compiler has to reject. Compile speed comes from
these rejection decisions **not depending on file order** — an unused import is visible while
that file alone is read, without waiting on how another file compiles.
Readability is what this lesson measures directly: all four rejections exist so that whoever
reads the source can read every name and every type **exactly as written**.

These three goals can appear to conflict with each other — writing less and writing intent
explicitly do not point the same way. The rest of this lesson measures that tension: do the
four rejected forms really block a shorter way of writing, or do they make visible something
that could have been left implicit?

## The Compiler Is Called From Inside the Program

The whole measurement comes down to a single question: does a piece of source text compile?
We answer that question without asking an external tool, using only the standard library's
ability to start a process and write files. The source text is written into a temporary
directory together with a module file, the compiler is run in that directory, and only the
exit status is read.

```go
// compiler.go — helper that calls the compiler from inside the program
package main

import (
	"os"
	"os/exec"
	"path/filepath"
)

func compiles(source string) bool {
	dir, err := os.MkdirTemp("", "trial")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dir)
	os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module trial\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dir, "code.go"), []byte(source), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dir
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func classifySource(source string) string {
	if compiles(source) {
		return "compiled"
	}
	return "not compiled"
}
```

**LB1.** The compiler here is not a concept but a call: `compiles` writes the source into a
temporary package, runs the compiler as a subprocess, and reads only the exit status. The
error text is never read, because the shape of an error message is version-dependent data; what
is measured through the entire course is not "why it was rejected" but "was it rejected".

This distinction has a cost: since `compiles` returns only `true` or `false`, a source could
have been rejected for a reason **other** than the one intended, and the measurement cannot
tell the difference. LB2 closes off that risk — each of the four sources is hand-built and
verified to compile if not for exactly one unwritten thing. This is a limit worth keeping in
mind while reading the four-line table shown in the lesson: `compiled`/`not compiled` is not a
debugging tool, it is a classification tool.

**LB2.** Each of the following four sources compiles if not for exactly one unwritten thing.
The rejection comes from that single cause — not from some other syntax error. This condition
holds throughout this lesson and the rest of the course wherever a rejection is measured.

Not needing an external tool secures two things at once. First, the measurement is
**deterministic**: the same source text gives the same answer every run, because the compiler
itself is called, not a separate interpreter mimicking it. Second, the measurement is
**portable**: `compiles` only uses the standard library's process-starting and
temporary-file-writing ability, and the rest of the course repeats this pattern.

## Four Rejections, Four Reasons

The following four sources are run side by side in the same frame. Each carries a gap that
would never cause a problem at runtime — none of them is an infinite loop, a memory bound
overrun, or a type mismatch. Yet none of the four compiles.

```go
// main.go — four rejection reasons run side by side
package main

import "fmt"

func main() {
	fmt.Printf("%-32s %s\n", "reason", "result")
	fmt.Printf("%-32s %s\n", "unused variable", classifySource(`package trial
func F() { x := 1 }`))
	fmt.Printf("%-32s %s\n", "unused import", classifySource(`package trial
import "fmt"
func F() {}`))
	fmt.Printf("%-32s %s\n", "unwritten return value", classifySource(`package trial
func F() int { }`))
	fmt.Printf("%-32s %s\n", "implicit type conversion", classifySource(`package trial
func F() { var a int32 = 1; var b int64 = a; _ = b }`))
}
```

```
reason                           result
unused variable                  not compiled
unused import                    not compiled
unwritten return value           not compiled
implicit type conversion         not compiled
```

All four lines give the same result, but they do not come from the same reason. **Unused
variable** and **unused import** reject a case where something written in the source is never
read at all — the language does not recognize a "maybe I will use it later" middle ground; a
name is either used or it is never written in the source at all. **The unwritten return value**
belongs to a different class: the signature promised an `int`, but the body produces no value,
and the compiler does not accept a body that fails to keep the promise. **The implicit type
conversion** is a third class: an `int32` cannot be assigned directly to an `int64` variable —
because the target's bit width differs from the source's, the compiler requires the widening
to be **written**.

What the three share is that none of them would lead to an observable error at runtime. An
unused variable does not crash the program, an unused import breaks nothing, widening from
`int32` to `int64` loses no data. The compiler is rejecting these not based on runtime risk but
based on **the source's readability**: an unused name misleads the reader, an implicit widening
hides from the reader which type stands where. The rejection here is not a safety measure, it
is a decision to **narrow** what stands in for the unwritten — the language chooses, in these
four cases, "never allow it" over "put something defined there".

Two of the rejections (unused variable, unused import) share one common escape route: the
language's **blank identifier**, `_`. Assigning a value to `_` is a written way of saying "I
know this value but I will not use it"; the compiler counts this as use and does not reject it.
This looks at first like a loophole that punches through the rejection rule, but it actually
does the opposite: writing `_` erases the difference between an unused name and one **silently
left as is**, because both become equally visible in the source — one a real name, the other a
deliberate "I do not care about this" marker. This identifier will reappear later in the
course: when skipping one of several return values, and when asking whether a key exists in a
map, the same word is used again.

Each rejection captures a concrete situation that arises during a fix. An unused import is a
line left behind when code is moved and a call is deleted; if the compiler let it stand, the
source would fill up over time with dependencies no longer needed, and seeing which import is
truly required would grow harder. An unused variable carries a similar trace: an intermediate
result bound to a name and then left unused is most often the mark of a half-finished edit. The
unwritten return value comes from a different source — the signature is a promise, the body
must keep it; the compiler enforces this to protect the assumption the caller built while
reading the signature. The implicit type conversion is the most indirect of all: even if two
integer types are in the same family, if their bit widths differ, silently copying one value
into the other means, from the reader's perspective, a widening that **never happened**; the
compiler prefers to make that widening visible rather than let it pass silently.

## Limiting Measurement: Rejecting Does Not Mean Writing Less

It would be misleading to think the rejection decision comes free. Take the rejection of the
implicit conversion: the line `var b int64 = a` does not compile when `a` is an `int32`. The
line that does the same job and does compile is `var b int64 = int64(a)` — the conversion call
**has to be written**. Compare the two lines directly.

```go
// limiter.go — rejecting does not mean writing less
package main

import "fmt"

func main() {
	implicitLine := "var b int64 = a"
	explicitLine := "var b int64 = int64(a)"

	implicitSource := "package trial\nfunc F() { var a int32 = 1; " + implicitLine + "; _ = b }"
	explicitSource := "package trial\nfunc F() { var a int32 = 1; " + explicitLine + "; _ = b }"

	fmt.Printf("%-38s %-13s length=%d\n", "implicit conversion", classifySource(implicitSource), len(implicitLine))
	fmt.Printf("%-38s %-13s length=%d\n", "explicit conversion", classifySource(explicitSource), len(explicitLine))
}
```

```
implicit conversion                    not compiled  length=15
explicit conversion                    compiled      length=22
```

The result looks reversed at first glance: the line that does not compile is **shorter** (15
characters), the line that does compile is **longer** (22 characters). Rejecting does not
shorten the source — it does the opposite. Rather than assume `a` will be widened, the compiler
wants that widening to be **visible** in the source, and it puts the cost of that on the party
writing it: a seven-character call.

**LB3.** The unit measured here is the character, not runtime or memory; the claim is not
"rejecting slows things down" but "rejecting does not shorten". A fifteen-character difference
seems small, but the same pattern repeats at every widening point in a source; the readability
gain is bought with a fixed writing cost per line.

The four do not all point the same direction. The fix for the unused import is a **deletion** —
the line disappears from the source entirely and the source shortens. The fix for the implicit
type conversion is an **addition** — a call wraps an existing line and the source grows. Both
rejections give the same class of result (not compiled), but the direction of the fix is
opposite. This breaks the impression that "rejecting" carries a single writing cost: some
rejection reasons push the source toward simplification, others push it toward writing intent
explicitly; the only thing they share is that neither leaves an **ambiguous range** in the
source.

The same holds for the fourth rejection (unused variable) in exactly the same way: deleting the
line `x := 1` shortens the source, but using `x` somewhere (writing `_ = x`) does not shorten
it either, it only adds a word. What stands in for rejection is never "write less"; it is
either delete or write your intent **explicitly**. Simplicity, counted among Go's design goals,
is not aimed at code length but at what appears in the source: a reader who sees the line
`int64(a)` knows the widening was deliberate; had they seen `var b int64 = a`, they could not
have known that.

This reading closes the tension opened earlier. Simplicity and writing intent explicitly
conflict at the level of the source's **length** but not at the level of its **rule count**:
the conversion rule the compiler recognizes is a single one — widening is always written — and
this single rule removes entirely from the reader the question "in which case is an implicit
conversion safe". The reader does not have to separately ask whether an assignment is implicit
or explicit, because the compiler already never lets the implicit one through. Simplicity here
sits not in a shrinking character count but in a shrinking number of **possible readings**.

The link to compile speed is built from the same point. None of the four rejections wait for
the rest of the source before deciding: an unused import is evident while that file alone is
being read; an implicit conversion is seen just by looking at the type on either side of the
assignment. Neither requires an inference dependent on what is happening elsewhere in the
program. Duration is never measured in this lesson, but the **locality** of the rejection
decisions — each looking only at its own expression — leaves an observable trace of where
compile speed comes from.

## Summary

- The compiler is called from inside the program in this lesson and throughout the rest of the
  course; only whether it passes is read, the error text is never read.
- An unused variable, an unused import, an unwritten return value, and an implicit type
  conversion — all four are harmless at runtime yet stop compilation.
- The three reasons fall into three distinct classes: a name written but never read, a body
  that fails to keep its promise, a widening invisible in the source.
- Rejecting the implicit conversion does not shorten the source: the line that fails to compile
  is 15 characters, its compiling counterpart is 22 characters.
- Rejection is a decision that narrows what stands in for the unwritten — in these four cases,
  the language chooses to never allow it rather than put a defined value there.

## Next Step

The four rejection reasons were about things a source writes **in excess** or leaves short of
a promise; none of them was about the smallest set of things a source **needs** in order to
run at all. The next lesson asks about that smallest set: which parts are mandatory for a
source to compile and run, and what does removing one of them cost when it is written not in
the source itself but in a module file?
