with one click
with one click
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | golang |
| description | Rules and best practices when writing and editing Go (Golang) code |
| metadata | {"relevant_files":["server/**/*.go","functions/**/*.go","cli/**/*.go"]} |
This codebases uses features from Go 1.25 and above.
DebugContext, InfoContext, WarnContext, ErrorContext.attr.SlogError(err). Example: logger.ErrorContext(ctx, "failed to write to database", attr.SlogError(err)).context.Context value as their first argument.mise lint:server to run the linters on the server codebase.exhaustruct linter requires all struct fields to be explicitly set in struct literals. When adding new fields to a type, update ALL call sites — including places that construct the struct with zero values (e.g., MyStruct{} → MyStruct{NewField: nil}).We use Goa to design our API and generate server code. All Goa code lives in server/design. The Goa DSL is documented in https://pkg.go.dev/goa.design/goa/v3/dsl.
To make an API change such as creating a new service or update an existing one:
server/design to reflect the API change.mise run gen:goa-serverserver/gen with the new API changes. It's best to use git to discover the added/changed files.When implementing Goa services:
server/internal/<service>/impl.go.package assets
import (
"context"
"log/slog"
goahttp "goa.design/goa/v3/http"
gen "github.com/speakeasy-api/gram/server/gen/assets"
srv "github.com/speakeasy-api/gram/server/gen/http/assets/server"
"github.com/speakeasy-api/gram/server/internal/auth"
)
type Service struct {
tracer trace.Tracer
logger *slog.Logger
auth *auth.Auth
// dependencies
}
func NewService(
logger *slog.Logger,
tracerProvider trace.TracerProvider,
auth *auth.Auth,
// dependencies
) *Service {
return &Service{
// initialize dependencies
}
}
var _ gen.Service = (*Service)(nil)
var _ gen.Auther = (*Service)(nil)
func Attach(mux goahttp.Muxer, service *Service) {
endpoints := gen.NewEndpoints(service)
endpoints.Use(middleware.MapErrors())
endpoints.Use(middleware.TraceMethods(service.tracer))
srv.Mount(
mux,
srv.New(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil),
)
}
func (s *Service) APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error) {
return s.auth.Authorize(ctx, key, schema)
}
func (s *Service) ListAssets(ctx context.Context, payload *gen.ListAssetsPayload) (*gen.ListAssetsResult, error) {
// implementation
}
If you are creating a new Goa service, then make sure to attach it to the http server in server/cmd/gram/start.go.
repo.New) when needed in functions.repo.Queries directly on a service struct for a new service.type Service struct {
queries *repo.Queries
}
func NewService(db *pgxpool.Pool) *Service {
return &Service{
queries: repo.New(db),
}
}
This makes the service depend on a concrete query helper instance up front, which is not the pattern we want for new services.
type Service struct {
db *pgxpool.Pool
}
func NewService(db *pgxpool.Pool) *Service {
return &Service{db: db}
}
func (s *Service) Handler(ctx context.Context) error {
queries := repo.New(s.db)
if err := queries.DoThing(ctx); err != nil {
return fmt.Errorf("do thing: %w", err)
}
return nil
}
This keeps the service dependency simple and avoids baking repo.Queries into the service shape.
authctx, assume ActiveOrganisationID is present.ActiveOrganisationID unless there is a concrete code path proving otherwise.Avoid patterns that treat ActiveOrganisationID as optional when reading authctx. That adds defensive code around an invariant that should already hold.
nil before calling it.deps.go based on c.String("environment").type Service struct {
client *vendor.Client
}
func NewService(cfg Config) *Service {
if cfg.APIKey == "" {
return nil
}
return &Service{client: vendor.New(cfg.APIKey)}
}
func (s *Service) Send(ctx context.Context, req *vendor.Request) error {
if s.client == nil {
return nil
}
return s.client.Send(ctx, req)
}
This leaks vendor types into internal code and spreads nil handling into runtime call paths.
type Client interface {
Send(ctx context.Context, message Message) error
}
type Message struct {
To string
Subject string
Body string
}
type Service struct {
client Client
}
func NewService(client Client) *Service {
return &Service{client: client}
}
Wire the real or stub implementation in deps.go so the service always receives a valid Client, and keep vendor-specific types inside the wrapper implementation.
func (s *Service) listWidgets(ctx context.Context) error {
return s.repo.ListWidgets(ctx)
}
func (s *Service) List(ctx context.Context) error {
return s.listWidgets(ctx)
}
The wrapper adds no abstraction and is only used once.
func (s *Service) List(ctx context.Context) error {
return s.repo.ListWidgets(ctx)
}
In low-level functions, use fmt.Errorf to wrap errors with distinct and useful context:
func SaveUser(repo Repository, u User) error {
err := repo.Save(u)
if err != nil {
return fmt.Errorf("failed to save user: %w", err)
}
return nil
}
Do not need to use "failed to" language.
func SaveUser(repo Repository, u User) error {
err := repo.Save(u)
if err != nil {
return fmt.Errorf("run database query: %w", err)
}
return nil
}
Do not use generic language that doesn't add any context and doesn't improving searching for errors in the codebase.
func SaveUser(repo Repository, u User) error {
err := repo.Save(u)
if err != nil {
return fmt.Errorf("save user: %w", err)
}
return nil
}
This is much better. The error message is concise and to the point and unique to the call site.
In higher-level functions of the server/ codebase, which include HTTP service handlers, use the server/internal/oops package which allows us to wrap internal errors with user-facing error messages.
func (s *Service) ListDeployments(ctx context.Context, form *gen.ListDeploymentsPayload) (res *gen.ListDeploymentResult, err error) {
var cursor uuid.NullUUID
if form.Cursor != nil {
c, err := uuid.Parse(*form.Cursor)
if err != nil {
return nil, oops.E(oops.CodeBadRequest, err, "invalid cursor").Log(ctx, s.logger)
}
cursor = uuid.NullUUID{UUID: c, Valid: true}
}
}
server/internal/attr/conventions.go when logging in the server codebase.logger.With(attr.SlogXXX(...)) to capture contextual attributes for logging in later parts of code.logger.InfoContext(ctx, "user created", "user_id", userID)
This is bad because it doesn't use the attributes from the convention package.
import "github.com/speakeasy-api/gram/functions/internal/attr"
func Example() {
logger.Error("failed to create user", attr.SlogError(err))
}
This is bad because it uses logger.Error instead of logger.ErrorContext.
import "github.com/speakeasy-api/gram/functions/internal/attr"
func Example(ctx context.Context) {
logger.ErrorContext(ctx, "failed to create user", attr.SlogError(err))
}
This is great because:
logger.ErrorContext which is the convention for logging in the server codebase.attr.SlogError attribute from the attr package.server/internal/conv)Use the conv package for common type conversions instead of writing inline helpers. Key functions:
conv.PtrEmpty(v) — If v is not the zero value, return a pointer to v; otherwise, return nil.conv.PtrValOr(ptr, default) — dereference a pointer with a fallback default.conv.Default(val, default) — return val unless it is the zero value, then return default.conv.ToPGText, conv.ToPGTextEmpty, conv.PtrToPGText, conv.PtrToPGTextEmpty — convert strings to pgtype.Text.conv.FromPGText, conv.FromPGBool — convert pgtype values to Go pointer types.conv.PtrToPGBool — convert a *bool to pgtype.Bool.conv.Ternary(cond, trueVal, falseVal) — inline conditional expression.Do NOT reimplement pointer helpers, ternary expressions, or pgtype conversions inline. Always reach for conv first.
server/internal/o11y)Use the o11y package for deferred cleanup and error logging. Two key functions:
o11y.LogDeferfunc LogDefer(ctx context.Context, logger *slog.Logger, cb func() error) error
Use LogDefer when a cleanup operation's error should be logged. Wrap cleanup calls with defer o11y.LogDefer(...) so failures are always visible in logs.
defer o11y.LogDefer(ctx, logger, func() error { return file.Close() })
o11y.NoLogDeferfunc NoLogDefer(cb func() error)
Use NoLogDefer when a cleanup operation's error can be silently discarded — for example, rolling back a database transaction (which is a no-op if the transaction already committed) or closing an HTTP response body.
dbtx, err := s.repo.DB().Begin(ctx)
if err != nil {
return nil, oops.E(oops.CodeUnexpected, err, "error accessing resource").Log(ctx, logger)
}
defer o11y.NoLogDefer(func() error { return dbtx.Rollback(ctx) })
defer o11y.NoLogDefer(func() error { return resp.Body.Close() })
o11y.LogDefer or o11y.NoLogDefer for deferred cleanup instead of bare defer resource.Close() calls. Bare defers silently discard errors with no traceability.LogDefer when the error matters for debugging (file I/O, critical resource cleanup).NoLogDefer when the error is expected or inconsequential (transaction rollbacks, response body closes).github.com/stretchr/testify/require exclusively.t.Context() instead of context.Background(), except inside t.Cleanup(func()) callbacks.t.Run to create subtests. Prefer writing separate test functions instead.setup_test.go files. Look for these across the codebase for inspiration and guidance.SELECT, INSERT, UPDATE, DELETE, transactions (Begin/BeginTx), CopyFrom, and SendBatch are all covered. Use SQLc-generated methods. Default to adding new fixture queries in the relevant domain package's own queries.sql (e.g. a toolsets-shaped fixture goes in server/internal/toolsets/queries.sql, not in testenv). Reach for server/internal/testenv/queries.sql (and testenv/testrepo) only when a fixture query is genuinely reused across multiple packages. The glint no-testing-raw-sql rule enforces this against *pgxpool.Pool, *pgx.Conn, pgx.Tx, and pgx.Querier receivers in *_test.go. ClickHouse uses a different driver and is not flagged.github.com/stretchr/testify/mock for mocking third-party libraries in tests instead of ad hoc fakes around vendor types.testenv.NewLogger(t), testenv.NewTracerProvider(t), and testenv.NewMeterProvider(t) instead of constructing loggers or noop OTel providers inline. testenv.NewLogger(t) discards in normal runs and emits pretty logs under go test -v, which inline slog.New(slog.DiscardHandler) and slog.New(slog.NewTextHandler(os.Stdout, nil)) do not. Exception: tests that assert on log output should use a capturing handler over a bytes.Buffer.ctx := context.Background()
This loses the test lifecycle context that Go now provides directly on *testing.T.
ctx := t.Context()
type mockEmailClient struct {
mock.Mock
}
func (m *mockEmailClient) Send(ctx context.Context, message Message) error {
args := m.Called(ctx, message)
return args.Error(0)
}
Use testify/mock when mocking integrations so expectations stay explicit and consistent across tests.