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.
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.
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
| Code | Belongs in | Why |
|---|---|---|
Target list |
| process wiring |
|
| domain behaviour |
| unexported | implementation detail |
JSON stdout |
| 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
Newthat 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.
