Awesome-omni-skill code-like-gopher
Provides Go programming expertise, including language syntax, idiomatic patterns, concurrency, and standard library usage. Use when generating, analyzing, refactoring, or reviewing Go code.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/code-like-gopher" ~/.claude/skills/diegosouzapw-awesome-omni-skill-code-like-gopher && rm -rf "$T"
skills/development/code-like-gopher/SKILL.mdGeneral Coding Approach
All the naming, commenting must be in English
When writing Go code, always check the Go version specified in the
go.mod
file and use language features that are available for that version.
For example, starting with Go 1.23, range-over-integers (iterator-style ranges) were introduced, making code like the following valid:
for i := range 10 { fmt.Println(i) }
Additionally, the long-standing loop variable capture issue in for loops has been fixed in modern Go versions. Each iteration now gets its own copy of the loop variable, so this pattern is safe and works as expected:
for i := range 10 { go func() { fmt.Println(i) }() }
Formatting and Linting the Code
Golangci-lint is the defacto tool for go source code formatting, linting and checking. First check if the tool is available:
command -v golangci-lint
if the
golangci-lint doesn’t exist, offer to install:
brew install golangci-lint # for macos + brew
Check if golangci-lint config file exists in the project, possible file names are:
.golangci.yml.golangci.yaml.golangci.toml.golangci.json
If the version of the config is not
"2", try to migrate configuration,
golangci-lint offers migrations via golangci-lint migrate -h
If config not available, here is your minimal
.golangci.yml config example:
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: use-waitgroup-go disabled: true - name: line-length-limit arguments: [120] - name: enforce-switch-style arguments: ["allowNoDefault"] - name: add-constant arguments: - max-lit-count: "3" allow-strs: '""' allow-ints: "0,1,2,10,64" allow-floats: "0.0,0.,1.0,1.,2.0,2." - name: comment-spacings - name: confusing-naming - name: datarace - name: context-as-argument - name: context-keys-type - name: deep-exit - name: defer arguments: - ["call-chain", "loop"] - name: duplicated-imports - name: early-return arguments: - "preserve-scope" - "allow-jump" - name: empty-block - name: empty-lines - name: error-naming - name: error-return - name: error-strings - name: errorf - name: exported - name: enforce-map-style arguments: - "make" - name: forbidden-call-in-wg-go - name: get-return - name: identical-branches - name: identical-ifelseif-branches - name: identical-ifelseif-conditions - name: identical-switch-branches - name: identical-switch-conditions - name: if-return - name: import-shadowing - name: increment-decrement - name: inefficient-map-lookup - name: modifies-parameter - name: modifies-value-receiver - name: optimize-operands-order - name: receiver-naming - name: redefines-builtin-id - name: redundant-import-alias - name: string-of-int - name: string-format - name: superfluous-else - name: time-date - name: time-equal - name: time-naming - name: unchecked-type-assertion arguments: - accept-ignored-assertion-result: true - name: unconditional-recursion - name: unexported-naming - name: unexported-return - name: unhandled-error - name: unnecessary-format - name: unreachable-code - name: unused-parameter arguments: - allow-regex: "^_" - name: unused-receiver arguments: - allow-regex: "^_" - name: use-errors-new - name: use-any - name: useless-break - name: var-declaration - name: var-naming arguments: - ["ID"] # AllowList - ["VM"] # DenyList - - skip-initialism-name-checks: true upper-case-const: true skip-package-name-checks: true skip-package-name-collision-with-go-std: true extra-bad-package-names: - helpers - utils - tools - models formatters: enable: - gofmt - gofumpt - goimports - golines settings: golines: max-len: 120
All the details of the configuration file can be found here: https://github.com/golangci/golangci-lint/blob/HEAD/.golangci.reference.yml
is your friend, use for formatting thegofmt
files*.go
helps you to sort and find required package importsgoimports
Help is available:
golangci-lint help formatters golangci-lint help linters golangci-lint fmt dir1 dir2/... golangci-lint fmt file1.go
Coding Style
Naming Conventions
Variable names should describe the value they hold, not the type of the variable. Incorrect (bad) examples include:
// Bad var userString string var countInt int var usersMap map[string]*User var companiesMap map[string]*Company var productsMap map[string]*Product var usersList []User // Good 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
Use predictable and easily understandable names:
- Use short variable names like
,i
,j
ink
loopsfor - Use
when representing a counter, total, or quantityn - In maps, use
for keys andk
for valuesv - Use
,a
for variables of the same type (e.g. during comparisons); their positions may be interchangeableb - Use
,x
as conventional names for locally scoped variables created for comparisonsy - Use
as a common shorthand for string valuess - Collections (
,map
,slice
) should always use plural namesarray
Functions should be named according to the result they return.
- A function name must start with a letter; it cannot start with a number and must not contain spaces
- Exported functions must start with an uppercase letter and must be documented with a comment
- Function names are case-sensitive
Examples:
func Add(a, b) int {} // describes only the operation // this is better func Sum(a, b) int {} // returned thing is a sum of a and b... // this describes the result, not the operation...
Method names should be named to describe the action they perform. This is the opposite of function naming:
package main import "fmt" type user struct { email string password string fullName string } // Email is a getter for user.email func (u user) Email() string { return u.email } // SetEmail is a setter for user.email func (u *user) SetEmail(email string) { u.email = email } // resetPassword resets user's password func (u *user) resetPassword() error { fmt.Println("example reset password") u.password = "reset" return nil } func main() { u := &user{} u.SetEmail("vigo@me.com") u.resetPassword() fmt.Println("email", u.Email()) fmt.Printf("%+v\n", u) }
Package Names
When a package is imported, the package name becomes an accessor for the contents. By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps.
Another convention is that the package name is the base name of its source directory; the package in
src/encoding/base64 is imported as "encoding/base64"
but has name base64, not encoding_base64 and not encodingBase64.
Don’t use the import . notation, which can simplify tests that must run outside the package they are testing, but should otherwise be avoided.
For instance, the buffered reader type in the
bufio package is called Reader,
not BufReader, because users see it as bufio.Reader, which is a clear, concise
name.
Moreover, because imported entities are always addressed with their package name,
bufio.Reader does not conflict with io.Reader. Similarly, the function
to make new instances of ring.Ring—which is the definition of a constructor in
Go—would normally be called NewRing, but since Ring is the only type exported
by the package, and since the package is called ring, it's called just New,
which clients of the package see as ring.New. Use the package structure to
help you choose good names.
Go doesn’t provide automatic support for getters and setters. There’s nothing wrong with providing getters and setters yourself, and it’s often appropriate to do so, but 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) }
Abbreviate judiciously. Package names may be abbreviated when the abbreviation is familiar to the programmer. Widely-used packages often have compressed names:
strconv (string conversion) syscall (system call) fmt (formatted I/O)
Don’t steal good names from the user. Avoid giving a package a name that is commonly used in client code. For example, the buffered I/O package is called bufio, not buf, since buf is a good variable name for a buffer.
Avoid repetition. Since client code uses the package name as a prefix when referring to the package contents, the names for those contents need not repeat the package name. 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() // start is a time.Time t, err := time.Parse(time.Kitchen, "6:06PM") // t is a time.Time ctx = context.WithTimeout(ctx, 10*time.Millisecond) // ctx is a context.Context ip, ok := userip.FromContext(ctx) // ip is a net.IP
A function named
New in package pkg returns a value of type pkg.Pkg. This is a
standard entry point for client code using that type:
q := list.New() // q is a *list.List
Write code that uses your package as a client would, and restructure your packages if the result seems poor. This approach will yield packages that are easier for clients to understand and for the package developers to maintain.
Avoid meaningless package names. Packages named
util, common, or misc
provide clients with no sense of what the package contains. This makes it
harder for clients to use the package and makes it harder for maintainers to
keep the package focused.
Over time, they accumulate dependencies that can make compilation significantly and unnecessarily slower, especially in large programs. And since such package names are generic, they are more likely to collide with other packages imported by client code, forcing clients to invent names to distinguish them.
Bad package name example:
package util func NewStringSet(...string) map[string]bool {...} func SortStringSet(map[string]bool) []string {...} set := util.NewStringSet("c", "a", "b") fmt.Println(util.SortStringSet(set))
Good package name example:
package stringset func New(...string) map[string]bool {...} func Sort(map[string]bool) []string {...} set := stringset.New("c", "a", "b") fmt.Println(stringset.Sort(set)) // Once you’ve made this change, it’s easier to see how to improve the new package: package stringset type Set map[string]bool func New(...string) Set {...} func (s Set) Sort() []string {...} set := stringset.New("c", "a", "b") fmt.Println(set.Sort())
Don’t use a single package for all your APIs. Many well-intentioned programmers put all the interfaces exposed by their program into a single package named
api, types, or interfaces, thinking it makes it easier to find
the entry points to their code base. This is a mistake. Such packages suffer
from the same problems as those named util or common, growing without bound,
providing no guidance to users, accumulating dependencies, and colliding with
other imports. Break them up, perhaps using directories to separate public
packages from implementation.
Interface names
By convention, one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun:
Reader, Writer,
Formatter, CloseNotifier etc.
type Stringer interface { String() string } type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } // FooBarBazer :) type ReadSeekCloser interface { Reader Seeker Closer }
Finally, the convention in Go is to use
MixedCaps or mixedCaps rather than
underscores to write multiword names.
Syntactic Sugars and Techniques
-
Try to avoid Naked Returns / Named Returns, instead of
use :func sum(a, b int) (result int)func sum(a, b int) (int) -
Embrace early exit approach
-
Instead of empty interface
useinterface{}
.any -
Compile time proof and interface checks are important
-
Make the zero value useful, stdlib’s
,bytes.Buffer
approachsync.Mutex -
Errors are values, use custom error types,
anderrors.As
your frienderrors.Is -
Every piece of code should be testable. Keep this in mind, use
approach to mock/dependency inject your types, functions etc...interface -
Try not to use generics, generics should be your last option!
-
When creating structs, keep field set alignment in your mind, order your fields properly, (golangci-linter has a linter - revive check field set alignment)
type size in bytes
byte, uint8, int8 1 uint16, int16 2 uint32, int32, float32 4 uint64, int64, float64, complex64 8 complex128 16
-
Never hardcode the sql variables in a query,
package provides lots of functions.database/sql -
Every type in go has a zero value never
usevar i int = 0var i int -
Keep pointer or value semantics in struct methods, if you don’t need to modify the data in struct, keep value semantics.
-
Never put
in struct fieldcontext.Context -
In Go,
must always be passed as the first argument to a function or method.context.Context
// Bad func FetchUser(id string, ctx context.Context) (*User, error) { // ... } // Good func FetchUser(ctx context.Context, id string) (*User, error) { // ... }
Don’t just check errors, handle them gracefully!
// bad error handling func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return err // ??? who fired this error? } return nil } // good error handling func AuthenticateRequest(r *Request) error { err := authenticate(r.User) if err != nil { return fmt.Errorf("authenticate failed: %v", err) // wrap errors with an extra message } return nil } // bad example func Write(w io.Writer, buf []byte) error { _, err := w.Write(buf) if err != nil { // annotated error goes to log file log.Println("unable to write:", err) // unannotated error returned to caller return err } return nil } // good example func Write(w io.Write, buf []byte) error { _, err := w.Write(buf) return errors.Wrap(err, "write failed") // <-- Wrap method from github.com/pkg/errors package }
Use functional options pattern instead of config structs:
// Option type definition type Option func(*Server) error // WithXxx functions - include validation func WithLogger(l *slog.Logger) Option { return func(s *Server) error { if l == nil { return fmt.Errorf("[server.WithLogger] error: %w", ErrNilLogger) } s.Logger = l return nil } } func WithPort(port string) Option { return func(s *Server) error { if port == "" { return fmt.Errorf("[server.WithPort] error: %w", ErrEmptyPort) } s.Port = port return nil } } // Constructor - defaults + options + validation func New(options ...Option) (*Server, error) { server := new(Server) // Set defaults server.Port = "8080" // Apply options for _, option := range options { if err := option(server); err != nil { return nil, err } } // Validate required fields if err := server.validate(); err != nil { return nil, err } return server, nil }
Test Conventions
Naming tests to self-document; instead of:
func TestTitleIllegalChar(t *testing.T) {}
Use:
func TestTitleEscape(t *testing.T) {}
With this rename, we also self-document how the illegal characters on the title will be handled.
Parallelize your table-driven tests;
func TestFoo(t *testing.T) { tc := []struct { dur time.Duration }{ {time.Second}, {2 * time.Second}, {3 * time.Second}, {4 * time.Second}, } for _, tt := range tc { tt := tt t.Run("", func(st *testing.T) { st.Parallel() time.Sleep(tt.dur) }) } }
Concurrency
REMEMBER! THE GOLDEN RULE
If you are the receiver (i.e. reading from a channel using
<-ch), never close the channel.
Only the sender (the one writing to the channel using ch <- value) should close it.
The goroutine that closes a channel must be the one that knows when the work is finished. In this case, the sender function (e.g.
count()) knows exactly when the loop ends —
therefore, it is the one responsible for closing the channel.
The sender always knows how many goroutines are writing to the channel, but the receiver does not. For this reason, a channel must always be closed by the sender, never by the receiver.
Always keep this in mind, be concurrent safe, use
sync.Map if you need concurrent
safe maps, way better that custom mutex guards.
When possible, prefer using a buffered channel with capacity
1 to avoid
unnecessary blocking and reduce tight synchronization between goroutines.
// Unbuffered (can cause unnecessary blocking) ch := make(chan int) // Preferred when only a signal or single value is needed ch := make(chan int, 1) // use cases // signal / done channel // error propagation // goroutine lifecycle coordination
Pre-Commit Hooks
If pre-commit config file
.pre-commit-config.yaml doesn’t exists, ask user to
install pre-commit:
brew install pre-commit
and install hooks:
pre-commit install
Use this minimum config:
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 Message Guidelines
- Prefix:
or[claude-opus]:
based on model[claude-sonnet]: - Short summary in lowercase (max 50 chars)
- Blank line, then bullet points with details
- Include Claude Code footer
- Use present tense in commit messages.
- Always start the message with a lowercase letter.
- Start with a verb, followed by a brief and clear description.
Examples:
fix login redirect issueimplement user profile pageremove unused dependencies
If related to a GitHub issue:
- Add
orFixes #ISSUE-NUMBER
at the end of the commit message. This auto closes issue on GitHub.Closes #ISSUE-NUMBER - Include a direct link to the related GitHub issue.
Also this commit-template is helpful:
# # 3456789x123456789x123456789x123456789x123456789x # Short description (subject) : 50 chars # 3456789x123456789x123456789x123456789x123456789x123456789x123456789x12 # Long description : 72 chars # # - Why was this change necessary? # - How does it address the problem? # - Are there any side effects? # # Fixes #ticket # Closes #ticket, #ticket, #ticket # # Include a link to the ticket, if any. #
Example:
[claude-opus]: add TXT and DOCX resume upload support - Convert TXT/DOCX files to PDF on upload using fpdf2 and python-docx - Skip text extraction for pre-extracted content - Add Unicode font support for Turkish characters 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>