The canonical .dockerignore and .gitignore both omitted the in-repo agent scratch directory. On this fleet that directory holds one worktree per in-flight agent -- an entire additional checkout of the repo each -- so under `COPY . .` all of it reached the build context and the image. Measured on this repo before the change: five planted scratch files, at every depth beneath the directory, all present inside a probe image built from the real context. Three consequences, only the first of which is about size. The context inflates by a multiple of the repo. Another session's unreviewed and sometimes uncommitted work is copied into a build artifact. And the directory is created and destroyed constantly by tooling, so it invalidates `COPY . .` for reasons that have nothing to do with this repo's content -- which is the accidental cache protection described at length in the issue thread, and the reason this change was sequenced behind the CHECK_EPOCH bust rather than landed alongside the rest of the .dockerignore work. The two entries are deliberately different shapes, because the two files have different semantics and neither is derived from the other. In .dockerignore the entry is anchored, `.claude`, with no `**/` prefix: the directory occurs exactly once, at the context root, and the prefixed form additionally matches any nested directory of that name. Measured rather than argued -- the `**/`-prefixed control was built and enumerated too, and it removes prompts/.claude/ from the context as well, which in a repo with a legitimately named nested directory would silently delete it from the build. In .gitignore the entry is unanchored, `.claude/`, because a .gitignore pattern already matches at every depth; `git check-ignore -v` confirms it covering both .claude/ and prompts/.claude/, so a `**/` prefix there would be redundant at best, and on an anchored pattern it would be actively wrong. Anchoring buys that at the cost of a residual exposure, and the vendored files now say so rather than only asserting the reason to anchor. "Occurs exactly once, at the context root" is a property of how agents are run, not of the tooling: the directory is created in the agent's working directory, so a monorepo running a per-service agent in services/api/ still ships services/api/.claude/ into the context and the image -- the exact exposure this change exists to close, left open in the repo shape where it is likeliest. The .dockerignore header block, the REPO_POLICIES.md bullet and both checklists state the gap and the remedy (anchored entries for the subdirectories that have one, or `**/.claude` once no legitimately named nested directory would be caught). Consuming repos receive the files and not the tracker, so a caveat that lives only in a PR body is not a caveat. It is not case-folded the way the neighbouring secret patterns are: tooling creates the directory in exactly one spelling, so a folded pattern would add no coverage. The earlier justification -- that a miss costs bloat rather than exposure -- is gone, because it contradicted this issue's own framing, in which the cost of a miss is unreviewed work in an image layer. The second half of this change is the consequence that ships broken silently. Excluding .git means `git describe` cannot run in any build stage, and it fails quietly there rather than erroring: `-X main.Version=` comes out empty, the binary reports no version, and the build still exits 0. The Go template in REPO_POLICIES.md had `ARG VERSION=dev` and never said where VERSION came from, which is precisely the gap a reader fills in with `git describe` inside the build. It now says: computed on the host, threaded in with `--build-arg VERSION=...`, shown as a complete command rather than as two rules each documenting half of one. script/docker and script/cibuild do it, with the same discipline the epoch already has -- assignment on its own line, because a failing command substitution inside an argument does not trip `set -e`, plus a non-empty fallback so a build from an export with no .git reports `unknown` rather than an empty string that reads as a successful version. That fallback is applied in exactly one place, and that place can actually execute. `git describe ... || true` leaves the value empty when it fails, and the `[ -n "$version" ]` line is what substitutes `unknown`. Folding the fallback into the substitution as `|| echo unknown` would have left the guard unreachable -- harmless in itself, but a guard that cannot fire is indistinguishable from one that works to every repo that copies it, and canonical text should not carry a check that is decorative. Measured firing in both sh and dash: not a git repository -> unknown, repository with no commits yet -> unknown, this repository -> the describe output. The checklist now carries the guard line as well, so the vendored guidance and the vendored script no longer disagree. The scripts pass VERSION unconditionally rather than growing a per-repo variant. This repo's Dockerfile declares no `ARG VERSION`, and BuildKit was measured accepting the unconsumed arg silently -- no warning, no cache effect, confirmed by the paired runs below in which the bootstrap layer still caches. The alternative, leaving it to each repo, reintroduces the trap: a repo that needs a version and finds no VERSION in its scripts writes `git describe` into the Dockerfile, which is the failure being closed. The same correction reaches the two Go documents that carry the GOLDFLAGS pattern, since a `$(shell git describe)` evaluated inside a build stage is exactly this empty version. Both are now `?=`, and their comments say precisely when that matters: where a build stage compiles by invoking make, `ARG VERSION` puts the value in the environment and `?=` defers to it, whereas the canonical Go template compiles with `go build` directly and uses the Makefile on the host only -- still `?=`, so that a repo which later moves its build behind make does not silently start shipping an empty version. Leaving them as `:=` would have left the corpus telling a reader one thing in the policy and the opposite in the styleguide. Both repo checklists gain the entries too. They are what an agent reads while writing these files, so they are where the wrong shape actually gets written: the .gitignore item is the one an existing repo never re-fetches, and it now names `.claude/` explicitly along with the warning not to prefix it. Verification, by enumerating a probe image rather than by reading the patterns. Standalone minimal Dockerfile held outside the context, `--no-cache` scoped to that one image, no prune of any kind. Before: all five planted scratch files in the image, 42 files total. After: zero, 37 files total, with README.md, script/check, prompts/NEW_REPO_CHECKLIST.md and a planted probe_src/app.md all still present as positive controls, so the exclusion is a real exclusion and not a COPY that stopped copying. Transferred context fell from 161.86kB to 68.82kB, recorded as corroboration only: BuildKit reports a delta, not a total, and an earlier run in this repo transferred 2.18kB while shipping 43 files. The CHECK_EPOCH verification was re-run under the changed context, because the context moved underneath the earlier measurement. Two consecutive script/cibuild runs on an unchanged tree: run 1 in 17.07s, run 2 in 5.73s, both executing the check layer with a distinct epoch and real prettier output from both lint and fmt-check. The `RUN script/bootstrap` layer is CACHED in run 2, which is the validity control -- it proves no concurrent prune landed between the runs and that no --no-cache path was taken, so the check layer executing is the bust working rather than a cold cache. Planted files were removed afterwards and their absence confirmed against the filesystem with `find`, not against `git status`, which cannot see them once .gitignore covers the directory -- the same blind spot that made the earlier secret exposure invisible.
25 KiB
title, last_modified
| title | last_modified |
|---|---|
| Code Styleguide — Go | 2026-08-09 |
-
Try to hard wrap long lines at 77 characters or less.
-
Don't commit anything that hasn't been
go fmt'd. The only exception is when committing things that aren't yet syntactically valid, which should only happen pre-v0.0.1 or on a non-mainbranch. -
Even if you are planning to deal with only positive integers, use
int/int64types instead ofuint/uint64types. This is for consistency and compatibility with the standard library; it's better than casting all the time. -
Any project that has more than 2 or 3 modules should use the
go.uber.org/fxdependency injection framework to keep things tidy. -
If you have to choose between readable and clever, opt for readable. It's ok to make the code less concise or slightly less idiomatic if you can keep it dead simple.
-
Embed the git commit hash into the binary and include it in startup logs and in health check output. This is to make it easier to correlate running instances with their code. Do not include build time or build user, as these will make the build nondeterministic.
Example relevant Makefile sections:
Given a
main.golike:package main import ( "fmt" ) var ( Version string Buildarch string ) func main() { fmt.Printf("Version: %s\n", Version) fmt.Printf("Buildarch: %s\n", Buildarch) }# ?= rather than := because this `$(shell git describe ...)` is only # correct on the host. `.dockerignore` excludes `.git`, so evaluated # inside a build stage it expands to the empty string without failing # and the binary reports no version at all. The version is computed on # the host by `script/docker` / `script/cibuild` and passed with # `--build-arg VERSION=...`. If this repo's Dockerfile compiles by # invoking make (`RUN make build`), `ARG VERSION` in that stage puts the # value in the environment and `?=` defers to it. The canonical Go # template in REPO_POLICIES.md instead runs `go build` directly with # `-ldflags "... -X main.Version=${VERSION}"`, so there this Makefile is # a host-only path — but it is still `?=`, because a repo that later # moves the build behind make must not silently start shipping an empty # version. See the git-describe rule in REPO_POLICIES.md. VERSION ?= $(shell git describe --always --dirty) BUILDARCH := $(shell uname -m) GOLDFLAGS += -X main.Version=$(VERSION) GOLDFLAGS += -X main.Buildarch=$(BUILDARCH) # osx can't statically link apparently?! ifeq ($(UNAME_S),Darwin) GOFLAGS := -ldflags "$(GOLDFLAGS)" endif ifneq ($(UNAME_S),Darwin) GOFLAGS = -ldflags "-linkmode external -extldflags -static $(GOLDFLAGS)" endif ./httpd: ./pkg/*/*.go ./internal/*/*.go cmd/httpd/*.go go build -o $@ $(GOFLAGS) ./cmd/httpd/*.go -
Avoid obvious footguns. For example, use range instead of for loops for iterating.
-
Use
log/slogfor structured logging. Importsneak.berlin/go/simplelogfor sensible defaults. Example:package main import ( "log/slog" _ "sneak.berlin/go/simplelog" ) func main() { slog.Info("Starting up") } -
Commit at least a single test file to check compilation. The test file can be empty, but it should exist. This is to ensure that
go test ./...will always function as a syntax check at a minimum. -
Full TDD and coverage isn't that important, but when fixing a specific bug, try to write a test that reproduces the bug before fixing it. This will help ensure that the bug doesn't come back later, and crystallizes the experience of discovering the bug and the resulting fix into the repository's history.
-
For anything beyond a simple script or tool, or anything that is going to run in any sort of "production" anywhere, make sure it passes
golangci-lint. -
Write a
Dockerfilefor every repo, even if it only runs the tests and linting.script/cibuildandscript/dockershould always make sure that the code is in an able-to-be-compiled state, linted, and any tests run, and the build should fail if linting doesn't pass. That guarantee holds only because those scripts pass a per-invocationCHECK_EPOCHbuild arg that busts the check layers out of the Docker cache; without it an unchanged tree serves those layers from cache and the build reports a green it never ran. A baredocker build .fails closed by design, on the[ -n "$CHECK_EPOCH" ]guard — always go throughscript/cibuildorscript/docker. See Repository Policies for the canonical form. -
Every repo must have a
Makefile. See Repository Policies for required targets and conventions. -
If you are writing a single-module library,
.gofiles are okay in the repo root. -
If you are writing a multi-module project, put all
.gofiles in apkg/orinternal/subdirectory.internal/is for modules used only by the current repo, andpkg/is for modules that can be consumed externally. This is to keep the repo root as clean as possible. -
Binaries go in
cmd/directories. Each binary should have its own directory. This is to keep the root clean and to make it easier to see what is a library and what is a binary. Only packagemainfiles should be incmd/*directories. -
Keep the
main()function as small as possible. -
Keep the
mainpackage as small as possible. Move as much code as is feasible to a library package, even if it's an internal one.mainis just an entrypoint to your code, not a place for implementations. Exception: single-file scripts. -
HTTP HandleFuncs should be returned from methods or functions that need to handle HTTP requests. Don't use methods or your top level functions as handlers.
-
Provide a .gitignore file that ignores at least
*.log,*.out, and*.testfiles, as well as any binaries. -
Constructors must be called
New().modulename.New()works great if you name the packages properly. If the constructor creates an instance from an existing value or representation,From<Something>()(e.g.FromBytes(),FromConfig()) is also acceptable. If the package contains multiple types andNew()is ambiguous,NewThing()is occasionally acceptable — but prefer restructuring packages so each type gets its own package and a plainNew(). Do not invent creative constructor names likeCreate(),Make(),Build(),Open()(unless wrapping an OS resource), orInit(). If you see a constructor with a non-standard name, rename it. -
Don't make packages too big. Break them up.
-
Don't make functions or methods too big. Break them up.
-
Use descriptive names for functions and methods. Don't be afraid to make them a bit long.
-
Use descriptive names for modules and filenames. Avoid generic names like
server.utilis banned. -
Constructors must take a
Paramsstruct (orThingParamswhenNewThing()is used), even for a single argument. Named fields in a Params struct are always clearer than positional arguments. Positional arguments for constructors are an endless source of bugs — they make call sites unreadable, invite wrong-order errors that the compiler can't catch when types coincide, and force every caller to update when a new field is added. The only exception is when the single argument is stupidly obvious from context — e.g.featureflag.New(true)orthing.NewFromReader(r). When in doubt, use a Params struct. -
Use
context.Contextfor all functions that need it. If you don't need it, you can passcontext.Background(). Anything long-running should get and abide by a Context. A context does not count against your number of function or method arguments for purposes of calculating whether or not you need a Params struct, because thectxis always first. -
Contexts are always named
ctx. -
Use
context.WithTimeoutorcontext.WithDeadlinefor any function that could potentially run for a long time. This is especially true for any function that makes a network call. Sane timeouts are essential. -
If a structure/type is only used in one function or method, define it there. If it's used in more than one, define it in the package. Keep it close to its usages. For example:
func (m *Mothership) tvPost() http.HandlerFunc { type MSTVRequest struct { URL string `json:"URL"` } type MSTVResponse struct { } return func(w http.ResponseWriter, r *http.Request) { // parse json from request var reqParsed MSTVRequest err = json.NewDecoder(r.Body).Decode(&reqParsed) ... if err != nil { SendErrorResponse(w, MSGenericError) return } log.Info().Msgf("Casting to %s: %s", tvName, streamURL) SendSuccessResponse(w, &MSTVResponse{}) } } -
Avoid global state, especially global variables. If you need to store state that is global to your launch or application instance, use a package
globalsorappstatewith a struct and a constructor and require it as a dependency in your constructors. This will allow consumers to be more easily testable and will make it easier to reason about the state of your application. Alternately, if your dependency graph allows for it, put it in the main struct/object of your application, but remember that this harms testability. -
Package-global "variables" are ok if they are constants, such as static strings or integers or errors.
-
Whenever possible, avoid hardcoding numbers or values in your code. Use descriptively-named constants instead. Recall the famous SICP quote: "Programs must be written for people to read, and only incidentally for machines to execute." Rather than comments, a descriptive constant name is much cleaner.
Example:
const jsonContentType = "application/json; charset=utf-8" func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) { w.WriteHeader(status) w.Header().Set("Content-Type", jsonContentType) ... } -
Define your struct types near their constructors.
-
Do not create packages whose sole purpose is to hold type definitions. Packages named
types,domain, ormodelsthat contain only structs and interfaces (with no behavior) are a code smell. Define types alongside the code that uses them. Type-only packages force consuming packages into alias imports and circular-dependency gymnastics, and indicate that the package boundaries were drawn around nouns instead of responsibilities. If multiple packages need the same type, put it in the package that owns the behavior, or in a small, focused interface package — not in a grab-bag types package. -
When defining custom string-based types (e.g.
type ImageID string), implementfmt.Stringer. Use.String()at SDK and library boundaries instead ofstring(v). This makes type conversions explicit, grep-able, and consistent across the codebase. Example:type ContainerID string func (id ContainerID) String() string { return string(id) } // At the Docker SDK boundary: resp, err := c.docker.ContainerStart(ctx, id.String(), opts) -
Define your interface types near the functions that use them, or if you have multiple conformant types, put the interface(s) in their own file.
-
Define errors as package-level variables. Use a descriptive name for the error. Use
errors.Newto create the error. If you need to include additional information in the error, use a struct that implements theerrorinterface. -
Use lowerCamelCase for local function/variable names. Use UpperCamelCase for type names, and exported function/variable names. Use snake_case for JSON keys. Use lowercase for filenames.
-
Explicitly specify UTC for datetimes unless you have a very good reason not to. Use
time.Now().UTC()to get the current time in UTC. -
String dates should always be ISO8601 formatted. Use
time.Time.Formatwithtime.RFC3339to get the correct format. -
Use
time.Timefor all date and time values. Do not useint64orstringfor dates or times internally. -
When using
time.Timein a struct, use a pointer totime.Timeso that you can differentiate between a zero value and a null value. -
Use
time.Durationfor all time durations. Do not useint64orstringfor durations internally. -
When using
time.Durationin a struct, use a pointer totime.Durationso that you can differentiate between a zero value and a null value. -
Whenever possible, in argument types and return types, try to use standard library interfaces instead of concrete types. For example, use
io.Readerinstead of*os.File. Tailor these to the needs of the specific function or method. Examples:-
io.Readerinstead of*os.File:io.Readeris a common interface for reading data, which can be implemented by many types, including*os.File,bytes.Buffer,strings.Reader, and network connections likenet.Conn.
-
io.Writerinstead of*os.Fileor*bytes.Buffer:io.Writeris used for writing data. It can be implemented by*os.File,bytes.Buffer,net.Conn, and more.
-
io.ReadWriterinstead of*os.File:io.ReadWritercombinesio.Readerandio.Writer. It is often used for types that can both read and write, such as*os.Fileandnet.Conn.
-
io.Closerinstead of*os.Fileor*net.Conn:io.Closeris used for types that need to be closed, including*os.File,net.Conn, and other resources that require cleanup.
-
io.ReadCloserinstead of*os.Fileorhttp.Response.Body:io.ReadClosercombinesio.Readerandio.Closer, and is commonly used for types like*os.Fileandhttp.Response.Body.
-
io.WriteCloserinstead of*os.Fileor*gzip.Writer:io.WriteClosercombinesio.Writerandio.Closer. It is used for types like*os.Fileandgzip.Writer.
-
io.ReadWriteCloserinstead of*os.Fileor*net.TCPConn:io.ReadWriteClosercombinesio.Reader,io.Writer, andio.Closer. Examples include*os.Fileandnet.TCPConn.
-
fmt.Stringerinstead of implementing a customStringmethod:fmt.Stringeris an interface for types that can convert themselves to a string. Any type that implements theString() stringmethod satisfies this interface.
-
errorinstead of custom error types:- The
errorinterface is used for representing errors. Instead of defining custom error types, you can use theerrors.Newfunction or thefmt.Errorffunction to create errors.
- The
-
net.Conninstead of*net.TCPConnor*net.UDPConn:net.Connis a generic network connection interface that can be implemented by TCP, UDP, and other types of network connections.
-
http.Handlerinstead of custom HTTP handlers:http.Handleris an interface for handling HTTP requests. Instead of creating custom handler types, you can use types that implement theServeHTTP(http.ResponseWriter, *http.Request)method.
-
http.HandlerFuncinstead of creating a new type:http.HandlerFuncis a type that allows you to use functions as HTTP handlers by implementing thehttp.Handlerinterface.
-
encoding.BinaryMarshalerandencoding.BinaryUnmarshalerinstead of custom marshal/unmarshal methods:- These interfaces are used for binary serialization and deserialization. Implementing these interfaces allows types to be encoded and decoded in a standard way.
-
encoding.TextMarshalerandencoding.TextUnmarshalerinstead of custom text marshal/unmarshal methods:- These interfaces are used for text-based serialization and deserialization. They are useful for types that need to be represented as text.
-
sort.Interfaceinstead of custom sorting logic:sort.Interfaceis an interface for sorting collections. By implementing theLen,Less, andSwapmethods, you can sort any collection using thesort.Sortfunction.
-
flag.Valueinstead of custom flag parsing:flag.Valueis an interface for defining custom command-line flags. Implementing theStringandSetmethods allows you to use custom types with theflagpackage.
-
-
Avoid using
panicin library code. Instead, return errors to allow the caller to handle them. Reservepanicfor truly exceptional conditions. -
Use
deferto ensure resources are properly cleaned up, such as closing files or network connections. Placedeferstatements immediately after resource acquisition. -
When calling a function with
go, wrap the function call in an anonymous function to ensure it runs in the new goroutine context:Right:
go func() { someFunction(arg1, arg2) }()Wrong:
go someFunction(arg1, arg2) -
Use
iotato define enumerations in a type-safe way. This ensures that the constants are properly grouped and reduces the risk of errors.Example:
type HandScore int const ( ScoreHighCard = HandScore(iota * 100_000_000_000) ScorePair ScoreTwoPair ScoreThreeOfAKind ScoreStraight ScoreFlush ScoreFullHouse ScoreFourOfAKind ScoreStraightFlush ScoreRoyalFlush )Example 2:
type ByteSize float64 const ( _ = iota // ignore first value by assigning to blank identifier KB ByteSize = 1 << (10 * iota) MB GB TB PB EB ZB YB ) -
Don't hardcode big lists of things in your normal code. Either isolate lists in their own module/package and write some getters, or use a third party library. For example, if you need a list of country codes, you can use https://github.com/emvi/iso-639-1. It's okay to embed a data file (use
go embed) in your binary if you need to, but make sure you parse it once as a singleton and don't read it from disk every time you need it. Don't use too much memory for this, embedding anything more than perhaps 25MiB (uncompressed) is probably too much. Compress the file before embedding and uncompress during the reading/parsing step for efficiency. -
When storing numeric values that represent a number of units, either include the unit in the variable name (e.g.
uptimeSeconds,delayMsec,coreTemperatureCelsius), or use a type alias (that includes the unit name), or use a 3p library such as github.com/alecthomas/units for SI/IEC byte units, or github.com/bcicen/go-units for temperatures (and others). The type system is your friend, use it. -
Once you have a working program, run
go mod tidyto clean up yourgo.modandgo.sumfiles. Tag a v0.0.1 or v1.0.0. Push yourmainbranch and tag(s). Subsequent work should happen on branches so thatmainis "always releasable". "Releasable" in this context means that it builds and functions as expected, and that all tests and linting passes.
Other Golang Tips and Best Practices (Optional)
-
For any internet-facing http server, set appropriate timeouts and limits to protect against slowloris attacks or huge uploads that can consume server resources even without authentication.
Example to limit request body size:
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Limit the request body to 10MB r.Body = http.MaxBytesReader(w, r.Body, 10<<20) if err := r.ParseForm(); err != nil { http.Error(w, "Request body too large", http.StatusRequestEntityTooLarge) return } fmt.Fprintf(w, "Hello, World!") }) http.ListenAndServe(":8080", nil) }Example to set appropriate timeouts:
package main import ( "net/http" "time" ) func main() { server := &http.Server{ Addr: ":8080", ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, Handler: http.DefaultServeMux, } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World!") }) server.ListenAndServe() } -
When passing channels to goroutines, use read-only (
<-chan) or write-only (chan<-) channels to communicate the direction of data flow clearly. -
Use
io.MultiReaderto concatenate multiple readers andio.MultiWriterto duplicate writes to multiple writers. This can simplify the handling of multiple data sources or destinations. -
For simple counters and flags, use the
sync/atomicpackage to avoid the overhead of mutexes. -
When using mutexes, minimize the scope of locking to reduce contention and potential deadlocks. Prefer to lock only the critical sections of code. Try to encapsulate the critical section in its own function or method. Acquire the lock as the first line of the function, defer release of the lock as the second line of the function, and lines 3-5 should perform the task. Try to keep it as short as possible. Avoid using mutexes in the middle of a function. In short, build atomic functions.
-
Design types to be immutable where possible. This can help avoid issues with concurrent access and make the code easier to reason about.
-
Global state can lead to unpredictable behavior and makes the code harder to test. Use dependency injection to manage state.
-
Avoid using
initfunctions unless absolutely necessary as they can lead to unpredictable initialization order and make the code harder to understand. -
Provide comments for all public interfaces explaining what they do and how they should be used. This helps other developers understand the intended use.
-
Be mindful of resource leaks when using
time.Timerandtime.Ticker. Always stop them when they are no longer needed. -
Use
sync.Poolto manage a pool of reusable objects, which can help reduce GC overhead and improve performance in high-throughput scenarios. -
Avoid using large buffer sizes for channels. Unbounded channels can lead to memory leaks. Use appropriate buffer sizes based on the application's needs.
-
Always handle the case where a channel might be closed. This prevents panic and ensures graceful shutdowns.
-
For small structs, use value receivers to avoid unnecessary heap allocations. Use pointer receivers for large structs or when mutating the receiver.
-
Only use goroutines when necessary. Excessive goroutines can lead to high memory consumption and increased complexity.
-
Use
sync.Condfor more complex synchronization needs that cannot be met with simple mutexes and channels. -
Reflection is powerful but should be used sparingly as it can lead to code that is hard to understand and maintain. Prefer type-safe solutions.
-
Avoid storing large or complex data in context. Context should be used for request-scoped values like deadlines, cancellation signals, and authentication tokens.
-
Use
runtime.Callersandruntime.CallersFramesto capture stack traces for debugging and logging purposes. -
Use the
testing.TBinterface to write helper functions that can be used with both*testing.Tand*testing.B. -
Use struct embedding to reuse code across multiple structs. This is a form of composition that can simplify code reuse.
-
Prefer defining explicit interfaces in your packages rather than relying on implicit interfaces. This makes the intended use of interfaces clearer and the code more maintainable.
Author
License
MIT. See LICENSE.