Project challenges / verified progress
Beacon: build an SSL and uptime monitor in Go

The engineering notebook

A prober package with a clean API

How does a working script become a package that the rest of Beacon can safely call?

Loading statusStage 10 of 10

  • Workspace not ready
  • Agent not ready
Focus25:00
A small focus ritual

0 focus sessions completed. Every fourth session offers a longer break. Start each phase when you are ready.

Study time never unlocks verified lesson progress.

Loading...

Loading verified progress...

Loading GitHub account...
Phase 4 — From script to prober

Step 01 of 06

Learn the concept

A thousand-line main.go is not architecture; it is a hostage situation with an exit code. The rest of Beacon will need probing from a service, tests and eventually an API. That means the prober becomes a package with a small surface and private machinery.

COMMAND OVER PACKAGEcmd/beacondparse, call, encodethin maininternal/proberTarget Result ProberAPIprivate helpersresolve dial classifylowercasestandard librarynet tls http timedeps
The command sits above the package. It chooses inputs and output. The internal package exposes the nouns and verbs Beacon needs while hiding resolver details, TLS wiring and classification helpers that callers should not depend on.
Step 01

The ideas this is made of

A package API starts with the caller's sentence

The caller should be able to say: Check this target with this context and give me a result. That sentence suggests Target, Result, Prober and Check. It does not suggest exporting resolve, dialTLS or classifyFailure. Helpers can stay lowercase until a real caller needs them. Small APIs are easier to test and harder to misuse.

`internal/` gives repository-private code a compiler fence

A package under internal can be imported only by code rooted at the parent of that directory. github.com/you/beacon/cmd/beacond may import github.com/you/beacon/internal/prober; an unrelated module may not. The go command rejects invalid imports. That lets Beacon share code internally without publishing a public module API.

Capital letters are promises

In Go, Prober is exported and classifyFailure is not. Exported identifiers appear in docs, autocomplete and other packages. They should have comments because they are the package's front door. More importantly, callers build against them. Renaming or changing exported names is a breaking change; changing lowercase helpers is routine maintenance.

Constructors prevent half-built values

New is where defaults, validation and dependencies meet. A prober may need an HTTP client, concurrency limit or resolver later. Callers should not know which private fields must be non-nil. Options such as WithHTTPClient(server.Client()) let tests swap dependencies without weakening production TLS verification or exposing every field.

Thin main makes behaviour testable without a process

main should parse configuration, create a context, construct a prober, call it and decide stdout or exit status. The probing logic belongs in internal/prober, where tests can call it directly. Thick mains force tests to run processes and encourage copy-paste when another command needs the same behaviour. That is how scripts fossilise.

A small package API and a thin main
package counter

// Counter counts named events.
type Counter struct {
	values map[string]int
}

// New creates a Counter ready for use.
func New() *Counter {
	return &Counter{values: make(map[string]int)}
}

// Add records one event with the supplied name.
func (c *Counter) Add(name string) {
	c.values[name]++
}

// Value returns the count for name.
func (c *Counter) Value(name string) int {
	return c.values[name]
}

func normalise(name string) string {
	return name
}

The exported surface is the type, constructor and methods callers need. The helper stays lowercase. Beacon follows the same rule: export Target, Result, New and Check; keep URL parsing, dialing and classification private until a caller proves otherwise.

What belongs where

CodeBelongs inWhy

Target list

cmd/beacond

process wiring

Prober.Check

internal/prober

domain behaviour

resolve helper

unexported

implementation detail

JSON stdout

cmd/beacond

command policy

httptest client

constructor option

test dependency

What these are called on the job

  • Package — Go files in one directory that compile together behind one name.

  • Exported identifier — A capitalised Go name usable from another package.

  • Internal package — A package importable only from inside its parent tree.

  • Constructor — A function such as New that returns a ready value.

  • Option function — A constructor argument that changes configuration without exposing fields.

  • httptest — Go's package for local HTTP and HTTPS test servers.