| name | code-like-gopher |
| description | Provides Go programming expertise, including language syntax, idiomatic patterns, concurrency, and standard library usage. Use when generating, analyzing, refactoring, or reviewing Go code. |
When to Use
Use this skill when:
- Writing, reviewing, or refactoring Go code
- Setting up Go project structure and tooling
- Debugging concurrency issues
- Configuring linters and formatters
- Writing idiomatic Go code
Prerequisites Check
Before starting any Go work:
go version
grep '^go ' go.mod 2>/dev/null | awk '{print $2}'
command -v golangci-lint
Instructions
General Coding Approach
- All naming and comments must be in English
- Always check the Go version in
go.mod and use appropriate language features
- Starting with Go 1.22+, the loop variable capture issue is fixed
- Starting with Go 1.23+, range-over-integers is available:
for i := range 10 {
fmt.Println(i)
}
for i := range items {
go func() {
fmt.Println(i)
}()
}
Formatting and Linting
golangci-lint is the standard tool for Go code quality.
brew install golangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
Check for existing config files:
.golangci.yml / .golangci.yaml
.golangci.toml / .golangci.json
If config version is not "2", migrate:
golangci-lint migrate
Minimal .golangci.yml Config
version: "2"
run:
timeout: 5m
tests: false
linters:
enable:
- errcheck
- govet
- ineffassign
- staticcheck
- unused
- misspell
- unconvert
- unparam
- gosec
- prealloc
- revive
- wrapcheck
settings:
govet:
enable:
- assign
- appends
- bools
- defers
- shadow
- unmarshal
- waitgroup
- lostcancel
- slog
- unreachable
errcheck:
check-type-assertions: true
exclude-functions:
- fmt.Fprintln
- fmt.Fprintf
wrapcheck:
ignore-package-globs:
- encoding/*
- github.com/pkg/*
revive:
enable-all-rules: true
rules:
- name: package-comments
disabled: true
- name: cognitive-complexity
disabled: true
- name: cyclomatic
disabled: true
- name: function-length
disabled: true
- name: line-length-limit
arguments: [120]
- name: add-constant
arguments:
- max-lit-count: "3"
allow-strs: '""'
allow-ints: "0,1,2,10,64"
formatters:
enable:
- gofmt
- gofumpt
- goimports
- golines
settings:
golines:
max-len: 120
Usage
golangci-lint run ./...
golangci-lint fmt ./...
All the details of the configuration file can be found here:
https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml
gofmt is your friend, use for formatting the *.go files
goimports helps you to sort and find required package imports
Naming Conventions
Variables
Name variables by what they hold, not their type:
var userString string
var countInt int
var usersMap map[string]*User
var usersList []User
var username string
var count int
var users map[string]*User
var users []User
Short variable conventions:
i, j, k - loop indices
n - counter, total, quantity
k, v - map key/value
a, b - same-type comparisons
s - string values
err - errors
ctx - context
Collections always use plural names:
var userString string
var countInt int
var usersMap map[string]*User
var companiesMap map[string]*Company
var productsMap map[string]*Product
var usersList []User
var username string
var count int
var users map[string]*User
var users []User
var companies map[string]*Company
var companies []Company
var products []Product
Functions vs Methods
Functions - name by the result they return:
func Add(a, b int) int {}
func Sum(a, b int) int {}
Methods - name by the action they perform:
type User struct {
email string
}
func (u User) Email() string {
return u.email
}
func (u *User) SetEmail(email string) {
u.email = email
}
Packages
- Lowercase, single-word names
- No underscores or mixedCaps
- Package name = base directory name
- Avoid
util, common, misc, helpers, tools, models, api, types
or interfaces.
package string_utils
func NewStringSet(...string) map[string]bool {}
package stringset
func New(...string) Set {}
Constructor naming:
q := list.New()
r := ring.New(10)
Rules:
-
Don’t use the import . notation, which can simplify tests that must run
outside the package they are testing, but should otherwise be avoided.
-
Package names may be abbreviated when the abbreviation is familiar to the
programmer. stdlib includes:
strconv (string conversion)
syscall (system call)
fmt (formatted I/O)
-
Avoid repetition. The HTTP server provided by the http package is
called Server, not HTTPServer. Client code refers to this type
as http.Server, so there is no ambiguity.
-
Simplify function names. When a function in package pkg returns a value
of type pkg.Pkg (or *pkg.Pkg), the function name can often omit the
type name without confusion:
start := time.Now()
t, err := time.Parse(time.Kitchen, "6:06PM")
ctx = context.WithTimeout(ctx, 10*time.Millisecond)
ip, ok := userip.FromContext(ctx)
Structs
-
Go doesn’t provide automatic support for getters and setters. You can
provide getters and setters by your self, it's neither idiomatic nor
necessary to put Get into the getter's name. If you have a field called
owner (lower case, unexported), the getter method should be called Owner
(upper case, exported), not GetOwner. The use of upper-case names for
export provides the hook to discriminate the field from the method.
A setter function, if needed, will likely be called SetOwner.
Both names read well in practice:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Interfaces
One-method interfaces use -er suffix:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Stringer interface {
String() string
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
Error Handling
Always wrap errors with context:
if err != nil {
return err
}
if err != nil {
return fmt.Errorf("authenticate user %s: %w", userID, err)
}
Use errors.Is and errors.As for error checking:
if errors.Is(err, os.ErrNotExist) {
}
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println(pathErr.Path)
}
Custom error types:
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
func GetUser(id string) (*User, error) {
if user == nil {
return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
return user, nil
}
Functional Options Pattern
Use instead of config structs:
type Server struct {
port string
logger *slog.Logger
}
type Option func(*Server) error
func WithPort(port string) Option {
return func(s *Server) error {
if port == "" {
return errors.New("port cannot be empty")
}
s.port = port
return nil
}
}
func WithLogger(l *slog.Logger) Option {
return func(s *Server) error {
if l == nil {
return errors.New("logger cannot be nil")
}
s.logger = l
return nil
}
}
func New(opts ...Option) (*Server, error) {
s := &Server{
port: "8080",
}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, fmt.Errorf("apply option: %w", err)
}
}
return s, nil
}
server, err := New(
WithPort("3000"),
WithLogger(slog.Default()),
)
Concurrency
Golden Rule
Only the sender closes the channel, never the receiver.
The sender knows when work is finished; the receiver does not.
func produce(ch chan<- int) {
defer close(ch)
for i := range 10 {
ch <- i
}
}
func consume(ch <-chan int) {
for v := range ch {
fmt.Println(v)
}
}
Buffered Channels
Prefer buffered channel with capacity 1 for signals:
done := make(chan struct{}, 1)
errCh := make(chan error, 1)
Concurrent-Safe Maps
Use sync.Map for concurrent access:
var cache sync.Map
cache.Store("key", value)
if v, ok := cache.Load("key"); ok {
}
actual, loaded := cache.LoadOrStore("key", newValue)
Best Practices
- Zero values - Make them useful (like
bytes.Buffer, sync.Mutex)
- Context - Always first parameter, never in struct fields
- Avoid naked returns - Use explicit returns
- Use
any instead of interface{}
- Generics - Last resort, prefer interfaces
- Struct field alignment - Order by size (8, 4, 2, 1 bytes)
- Pointer vs value receivers - Be consistent per type
type Service struct {
ctx context.Context
}
func (s *Service) Do(ctx context.Context, id string) error {}
Compile-time interface checks:
var _ io.Reader = (*MyReader)(nil)
var _ http.Handler = (*MyHandler)(nil)
Testing
Naming
func TestTitleIllegalChar(t *testing.T) {}
func TestTitleEscapesSpecialCharacters(t *testing.T) {}
Table-Driven Tests
func TestSum(t *testing.T) {
t.Parallel()
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -1, -2},
{"zero", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := Sum(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Sum(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
Pre-Commit Hooks
brew install pre-commit
pre-commit install
Minimal .pre-commit-config.yaml:
repos:
- repo: https://github.com/TekWizely/pre-commit-golang
rev: v1.0.0-rc.1
hooks:
- id: golangci-lint-mod
- id: go-mod-tidy
- id: go-test-mod
Commit Messages
Format:
[claude]: <verb> <description in lowercase>
- Detail 1
- Detail 2
Fixes #123
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Example:
[claude]: add user authentication middleware
- Implement JWT validation
- Add rate limiting per user
- Handle token refresh
Fixes #42
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Quick Reference
| Task | Command |
|---|
| Check Go version | go version |
| Run linter | golangci-lint run ./... |
| Format code | golangci-lint fmt ./... |
| Run tests | go test -race ./... |
| Tidy modules | go mod tidy |
| Build | go build ./... |
Resources