// GOOD: Handle or propagate errors explicitly
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething failed: %w", err)
}
// GOOD: Sentinel errors for expected conditions
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) {
// handle expected case
}
// GOOD: Custom error types for rich context
type ScanError struct {
Host string
Port int
Err error
}
func (e *ScanError) Error() string {
return fmt.Sprintf("scan %s:%d: %v", e.Host, e.Port, e.Err)
}
func (e *ScanError) Unwrap() error { return e.Err }
Key principles:
- Wrap errors with
fmt.Errorf("context: %w", err) for stack context
- Use
errors.Is() and errors.As() for error inspection
- Define sentinel errors at package level for expected conditions
- Use custom error types when callers need structured error data
// GOOD: Small, focused interface defined by consumer
type HostResolver interface {
Resolve(hostname string) ([]net.IP, error)
}
// Consumer accepts the interface
func ScanHosts(resolver HostResolver, hosts []string) ([]Result, error) {
// ...
}
// sync.Mutex zero value is unlocked - ready to use
var mu sync.Mutex
// bytes.Buffer zero value is empty buffer - ready to use
var buf bytes.Buffer
project/
├── cmd/
│ └── toolname/
│ └── main.go # Entry point, flag parsing, minimal logic
├── internal/
│ ├── scan/ # Core scanning logic
│ │ ├── scanner.go
│ │ └── scanner_test.go
│ ├── report/ # Output formatting
│ │ ├── report.go
│ │ └── report_test.go
│ └── config/ # Configuration handling
│ └── config.go
├── go.mod
├── go.sum
├── Makefile
└── README.md
Key principles:
cmd/ contains minimal entry points that wire dependencies together
internal/ for packages private to this module
pkg/ only when explicitly designing a public API
- Keep
main.go thin -- parse flags, create dependencies, call into internal/
- One package per logical concern
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParsePort(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ParsePort(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ParsePort(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
<!-- markdownlint-enable MD040 MD046 -->
</pattern>
<pattern name="benchmark_tests">
```go
func BenchmarkScanPort(b *testing.B) {
scanner := NewScanner(DefaultConfig())
target := "127.0.0.1"
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = scanner.ScanPort(target, 80)
}
}
Run benchmarks: go test -bench=. -benchmem ./...
- Use
t.Helper() for cleaner error messages
- Use
t.Cleanup() or return cleanup functions
- Use
t.Parallel() for independent tests
- Use
testdata/ directory for test fixtures
func (m *mockResolver) Resolve(host string) ([]net.IP, error) {
return m.resolveFunc(host)
}
func TestScanWithResolver(t *testing.T) {
mock := &mockResolver{
resolveFunc: func(host string) ([]net.IP, error) {
return []net.IP{net.ParseIP("127.0.0.1")}, nil
},
}
// use mock in test
}
<!-- markdownlint-enable MD040 MD046 -->
</pattern>
</testing>
<lint_pitfalls>
Common golangci-lint failures to avoid proactively:
- **commentedOutCode (gocritic)**: Comments resembling code trigger this. Arithmetic-like comments in tests (e.g., `// Weight(15) + Weight(35) = 50`) are flagged. Use natural language instead: `// Expected: OUI weight plus BRIDGE-MIB weight`.
- **prealloc**: Preallocate slices when loop length is known: `make([]T, 0, len(source))` not `var slice []T` with append in a loop.
- **rangeValCopy (gocritic)**: Structs over 64 bytes copied per iteration. Use `for i := range slice` and access via `slice[i]`. Replace ALL loop variable references in the body.
- **httpNoBody (gocritic)**: Use `http.NoBody` instead of `nil` for GET/HEAD/DELETE requests without a body.
- **builtinShadow (gocritic)**: Never use Go builtins as parameter names (`new`, `make`, `len`, `copy`, `min`, `max`, `clear`). Rename to `n`, `count`, `limit`, `val`, etc.
- **nilerr**: When a function receives a non-nil error and returns `nil` as the error (encoding it into a result struct), the linter flags it. Return both result and wrapped error.
- **bodyclose**: Always close HTTP response bodies, including from `websocket.Dial()` responses.
</lint_pitfalls>
<success_criteria>
Go code produced with this skill should:
- Pass `go vet ./...` with no warnings
- Pass `golangci-lint run` with standard linters
- Have test coverage for exported functions
- Use `context.Context` for cancellable operations
- Handle all errors explicitly
- Follow standard project layout conventions
</success_criteria>
---
> Converted and distributed by [TomeVault](https://tomevault.io/claim/herbhall) — claim your Tome and manage your conversions.
<!-- tomevault:4.0:skill_md:2026-04-15 -->
1---2name: go-development-23description: Go development patterns, idioms, and conventions. Use when writing Go code, structuring Go modules, implementing error handling, writing tests, or building network and security tools in Go. Use when this capability is needed.4---56<objective>7Provides Go development expertise including idiomatic patterns, module organization, error handling, testing conventions, and patterns specific to network and security tooling.8</objective>910<idioms>1112<idiom name="error_handling">13Go uses explicit error returns. Never ignore errors silently.1415```go16// GOOD: Handle or propagate errors explicitly17result, err := doSomething()18if err != nil {19 return fmt.Errorf("doSomething failed: %w", err)20}2122// GOOD: Sentinel errors for expected conditions23var ErrNotFound = errors.New("not found")2425if errors.Is(err, ErrNotFound) {26 // handle expected case27}2829// GOOD: Custom error types for rich context30type ScanError struct {31 Host string32 Port int33 Err error34}3536func (e *ScanError) Error() string {37 return fmt.Sprintf("scan %s:%d: %v", e.Host, e.Port, e.Err)38}3940func (e *ScanError) Unwrap() error { return e.Err }41```4243Key principles:4445- Wrap errors with `fmt.Errorf("context: %w", err)` for stack context46- Use `errors.Is()` and `errors.As()` for error inspection47- Define sentinel errors at package level for expected conditions48- Use custom error types when callers need structured error data49</idiom>5051<idiom name="naming">52- Exported names: `PascalCase` (e.g., `ScanResult`, `NewScanner`)53- Unexported names: `camelCase` (e.g., `scanHost`, `portRange`)54- Interfaces: name by behavior, often single-method with `-er` suffix (`Scanner`, `Reader`, `Resolver`)55- Acronyms: all caps (`HTTP`, `URL`, `IP`, `TCP`, `DNS`)56- Package names: short, lowercase, no underscores (e.g., `scan`, `resolve`, `report`)57- Avoid stutter: `scan.Scanner` not `scan.ScanScanner`58</idiom>5960<idiom name="interfaces">61- Accept interfaces, return structs62- Keep interfaces small (1-3 methods preferred)63- Define interfaces where they are consumed, not where implemented6465```go66// GOOD: Small, focused interface defined by consumer67type HostResolver interface {68 Resolve(hostname string) ([]net.IP, error)69}7071// Consumer accepts the interface72func ScanHosts(resolver HostResolver, hosts []string) ([]Result, error) {73 // ...74}75```7677</idiom>7879<idiom name="zero_values">80Design types so the zero value is useful:8182```go83// sync.Mutex zero value is unlocked - ready to use84var mu sync.Mutex8586// bytes.Buffer zero value is empty buffer - ready to use87var buf bytes.Buffer88```8990</idiom>9192</idioms>9394<module_structure>95Standard Go project layout for CLI tools:9697```text98project/99├── cmd/100│ └── toolname/101│ └── main.go # Entry point, flag parsing, minimal logic102├── internal/103│ ├── scan/ # Core scanning logic104│ │ ├── scanner.go105│ │ └── scanner_test.go106│ ├── report/ # Output formatting107│ │ ├── report.go108│ │ └── report_test.go109│ └── config/ # Configuration handling110│ └── config.go111├── go.mod112├── go.sum113├── Makefile114└── README.md115```116117Key principles:118119- `cmd/` contains minimal entry points that wire dependencies together120- `internal/` for packages private to this module121- `pkg/` only when explicitly designing a public API122- Keep `main.go` thin -- parse flags, create dependencies, call into `internal/`123- One package per logical concern124</module_structure>125126<testing>127128<pattern name="table_driven_tests">129<!-- markdownlint-disable MD040 MD046 -->130```go131func TestParsePort(t *testing.T) {132 tests := []struct {133 name string134 input string135 want int136 wantErr bool137 }{138 {name: "valid port", input: "8080", want: 8080},139 {name: "min port", input: "1", want: 1},140 {name: "max port", input: "65535", want: 65535},141 {name: "zero port", input: "0", wantErr: true},142 {name: "negative port", input: "-1", wantErr: true},143 {name: "overflow", input: "65536", wantErr: true},144 {name: "non-numeric", input: "abc", wantErr: true},145 {name: "empty string", input: "", wantErr: true},146 }147148 for _, tt := range tests {149 t.Run(tt.name, func(t *testing.T) {150 got, err := ParsePort(tt.input)151 if (err != nil) != tt.wantErr {152 t.Errorf("ParsePort(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)153 return154 }155 if got != tt.want {156 t.Errorf("ParsePort(%q) = %v, want %v", tt.input, got, tt.want)157 }158 })159 }160}161162```163<!-- markdownlint-enable MD040 MD046 -->164</pattern>165166<pattern name="benchmark_tests">167```go168func BenchmarkScanPort(b *testing.B) {169 scanner := NewScanner(DefaultConfig())170 target := "127.0.0.1"171172 b.ResetTimer()173 for i := 0; i < b.N; i++ {174 _ = scanner.ScanPort(target, 80)175 }176}177```178179Run benchmarks: `go test -bench=. -benchmem ./...`180</pattern>181182<pattern name="test_helpers">183```go184// testutil.go in same package185func setupTestServer(t *testing.T) (addr string, cleanup func()) {186 t.Helper()187 ln, err := net.Listen("tcp", "127.0.0.1:0")188 if err != nil {189 t.Fatal(err)190 }191 return ln.Addr().String(), func() { ln.Close() }192}193```194195- Use `t.Helper()` for cleaner error messages196- Use `t.Cleanup()` or return cleanup functions197- Use `t.Parallel()` for independent tests198- Use `testdata/` directory for test fixtures199</pattern>200201<pattern name="mock_interfaces">202<!-- markdownlint-disable MD040 MD046 -->203```go204// Mock in test file205type mockResolver struct {206 resolveFunc func(string) ([]net.IP, error)207}208209func (m *mockResolver) Resolve(host string) ([]net.IP, error) {210 return m.resolveFunc(host)211}212213func TestScanWithResolver(t *testing.T) {214 mock := &mockResolver{215 resolveFunc: func(host string) ([]net.IP, error) {216 return []net.IP{net.ParseIP("127.0.0.1")}, nil217 },218 }219 // use mock in test220}221222```223<!-- markdownlint-enable MD040 MD046 -->224</pattern>225226</testing>227228<lint_pitfalls>229Common golangci-lint failures to avoid proactively:230231- **commentedOutCode (gocritic)**: Comments resembling code trigger this. Arithmetic-like comments in tests (e.g., `// Weight(15) + Weight(35) = 50`) are flagged. Use natural language instead: `// Expected: OUI weight plus BRIDGE-MIB weight`.232- **prealloc**: Preallocate slices when loop length is known: `make([]T, 0, len(source))` not `var slice []T` with append in a loop.233- **rangeValCopy (gocritic)**: Structs over 64 bytes copied per iteration. Use `for i := range slice` and access via `slice[i]`. Replace ALL loop variable references in the body.234- **httpNoBody (gocritic)**: Use `http.NoBody` instead of `nil` for GET/HEAD/DELETE requests without a body.235- **builtinShadow (gocritic)**: Never use Go builtins as parameter names (`new`, `make`, `len`, `copy`, `min`, `max`, `clear`). Rename to `n`, `count`, `limit`, `val`, etc.236- **nilerr**: When a function receives a non-nil error and returns `nil` as the error (encoding it into a result struct), the linter flags it. Return both result and wrapped error.237- **bodyclose**: Always close HTTP response bodies, including from `websocket.Dial()` responses.238</lint_pitfalls>239240<success_criteria>241Go code produced with this skill should:242243- Pass `go vet ./...` with no warnings244- Pass `golangci-lint run` with standard linters245- Have test coverage for exported functions246- Use `context.Context` for cancellable operations247- Handle all errors explicitly248- Follow standard project layout conventions249</success_criteria>250251---252> Converted and distributed by [TomeVault](https://tomevault.io/claim/herbhall) — claim your Tome and manage your conversions.253<!-- tomevault:4.0:skill_md:2026-04-15 -->