Go Function Design
A function's surface is read more often than its body. Optimize for the reader: predictable ordering in the file, signatures that scan, no hidden bool flags.
Core Rules
- Order by use, not alphabet. Types → constructors → exported methods → unexported → utilities.
- Keep signatures on one line when reasonable. When wrapping, every parameter on its own line with a trailing comma.
- Never pass
*Interface. Pass the interface value; the underlying data can already be a pointer. - Replace naked
bool/intparameters with named types or add/* name */comments at call sites. - Printf-style functions end in
fsogo vetcan check the format. - Prefer
%qover%splus manual quoting when formatting strings for errors and logs.
File Ordering
type Server struct{ ... }
func NewServer(...) *Server { ... } // constructor next to type
func (s *Server) Start(ctx context.Context) error { ... } // exported
func (s *Server) Stop() error { ... }
func (s *Server) acceptLoop() { ... } // unexported
func parseAddr(s string) (string, error) { ... } // file-local helper
Rules:
- Types and their constructors sit together at the top.
- Exported methods come before unexported ones.
- File-local helpers go at the bottom.
- Within a section, follow rough call order.
Signature Formatting
// Fits on one line — keep it on one line
func Sum(xs []int) int
// Too long — break with every param on its own line
func (r *Repo) SaveTransaction(
ctx context.Context,
userID string,
tx Transaction,
opts ...SaveOption,
) (string, error) {
...
}
The trailing comma is required and gofmt-stable.
Avoid Naked Bool/Int Parameters
// Bad — what does `true` mean?
NewServer(":8080", true, 30, false)
// Better — call-site comments
NewServer(":8080", true /* tls */, 30 /* maxConn */, false /* readonly */)
// Best — named types or options
NewServer(":8080", WithTLS(), WithMaxConn(30))
When a single bool is genuinely binary and obvious from the function name (SetVerbose(true)), it's fine.
Read references/signatures.md for return-value styles, naked returns, function-as-parameter formatting, and the variadic-options call-site shape.
Pointers to Interfaces
// Bad
func process(r *io.Reader) { ... }
// Good
func process(r io.Reader) { ... }
An interface value already carries a pointer-sized data word. *io.Reader is a pointer to an interface — almost always a mistake.
Printf and Stringer
Functions that accept a format string should end in f:
func Logf(format string, args ...any)
go vet then checks that %s, %d, etc. match the argument types.
When formatting strings into errors or logs, prefer %q:
return fmt.Errorf("unknown key %q", key) // unknown key "foo\nbar"
%q quotes and escapes; %s plus manual quoting ("key \"" + key + "\"") is fragile.
Read references/printf-and-stringer.md for
%vvs%svs%q, implementingfmt.Stringersafely, avoidingString()infinite recursion, andfmt.Formatter.
Variadic Options at the Call Site
db.Open(addr,
db.WithCache(false),
db.WithLogger(log),
db.WithRetries(3),
)
Each option on its own line, trailing comma. Use this layout whenever the call doesn't fit on a single line.
Constructors
A constructor immediately follows its type. Use the short form when no error is possible:
type Counter struct{ n int }
func NewCounter() *Counter { return &Counter{} }
Return an error when construction can fail:
func NewClient(addr string) (*Client, error) { ... }
Don't expose a half-built type through a constructor that "always succeeds" but requires Init() afterward.
Anti-Patterns
| Anti-pattern | Why it hurts | Do this instead |
|---|---|---|
| Methods scattered randomly in the file | Hard to navigate | Group by type, exported-first |
| Five-argument wrapped signature with no trailing comma | gofmt keeps reformatting |
Trailing comma |
func process(r *io.Reader) |
Pointer to interface | Pass io.Reader |
Log(msg string, format bool, ...) |
Combines two concerns; vet blind |
Separate Log and Logf |
Open(":8080", true, false, 30) |
Unreadable booleans | Named options or /* */ comments |
fmt.Errorf("got %s", key) for arbitrary key |
Special chars unclear in output | %q |
Returning *MyError (concrete pointer) |
Typed-nil interface trap | Return error |
Verification Checklist
- Types appear above their constructors; exported methods above unexported
- Long signatures wrap with one parameter per line and a trailing comma
- No pointer-to-interface parameters
- Bool/int parameters are either obvious from the function name or named with
/* */comments - Functions taking a format string end in
f - Errors and logs use
%qwhen formatting arbitrary strings - Constructors return
(*T, error)when construction can fail — no half-built objects
References
- references/signatures.md — multi-line wrapping, named results, function-typed parameters
- references/printf-and-stringer.md — format verbs,
fmt.Stringer, recursion traps,fmt.Formatter