Go Development Standards¶
Standards and best practices for Go development in the dotfiles project.
Project Structure¶
Follow the Standard Go Project Layout:
tools/sess/
├── cmd/ # Main applications
│ ├── root.go # Root command
│ ├── list.go # Subcommands
│ └── create.go
├── internal/ # Private application code
│ ├── config/ # Configuration parsing
│ ├── session/ # Business logic
│ ├── tmux/ # External integrations
│ └── ui/ # User interface
├── pkg/ # Public library code (if needed)
├── main.go # Entry point
├── go.mod
├── go.sum
├── README.md
└── .gitignore
Key Principles:
internal/prevents external imports (enforced by Go)cmd/contains CLI-specific code- Business logic goes in
internal/ - Only expose APIs in
pkg/if reusable by other projects
Dependencies¶
Minimize dependencies:
- Prefer standard library when possible
- Choose well-maintained, popular libraries
- Avoid bleeding-edge or experimental packages
Approved Dependencies:
Core:
gopkg.in/yaml.v3- YAML parsing (robust, stable)github.com/spf13/cobra- CLI framework (industry standard)github.com/charmbracelet/bubbletea- TUI frameworkgithub.com/charmbracelet/lipgloss- Terminal styling
Testing:
- Standard library
testing github.com/stretchr/testify(optional, for assertions)
Adding New Dependencies:
- Check if standard library suffices
- Research alternatives
- Verify maintenance status (commits, issues)
- Document reason in commit message
Code Style¶
Follow standard Go conventions:
gofmtfor formatting (automatic)golintfor lintinggo vetfor static analysis
Naming:
// Good
type SessionManager struct {}
func (sm *SessionManager) ListSessions() {}
// Bad
type session_manager struct {}
func (sm *session_manager) list_sessions() {}
Comments:
// Package session provides tmux session management.
package session
// Manager handles session creation and switching.
type Manager struct {
config *Config
}
// ListSessions returns all active tmux sessions.
// It returns an error if tmux is not running.
func (m *Manager) ListSessions() ([]Session, error) {
// Implementation
}
Error Handling:
// Good - wrap errors with context
if err := m.createSession(name); err != nil {
return fmt.Errorf("create session %q: %w", name, err)
}
// Bad - lose context
if err := m.createSession(name); err != nil {
return err
}
Testing¶
Test Coverage Goals:
- New code: >80% coverage
- Critical paths: 100% coverage
- UI/CLI: Integration tests
Test Organization:
internal/
├── config/
│ ├── config.go
│ └── config_test.go
├── session/
│ ├── manager.go
│ ├── manager_test.go
│ └── testdata/
│ └── sample_config.yml
Test Patterns:
Unit tests:
func TestSessionManager_ListSessions(t *testing.T) {
tests := []struct {
name string
setup func()
want []Session
wantErr bool
}{
{
name: "returns active sessions",
setup: func() {
// Setup mock
},
want: []Session{
{Name: "dotfiles", Windows: 3},
},
wantErr: false,
},
{
name: "returns error when tmux not running",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setup != nil {
tt.setup()
}
m := NewManager()
got, err := m.ListSessions()
if (err != nil) != tt.wantErr {
t.Errorf("ListSessions() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ListSessions() = %v, want %v", got, tt.want)
}
})
}
}
Table-driven tests for multiple cases:
func TestParseSessionConfig(t *testing.T) {
tests := []struct {
name string
input string
want *SessionConfig
wantErr bool
}{
{
name: "valid config",
input: `
defaults:
- name: test
directory: /tmp
`,
want: &SessionConfig{
Defaults: []Session{
{Name: "test", Directory: "/tmp"},
},
},
wantErr: false,
},
{
name: "invalid YAML",
input: "invalid: [",
wantErr: true,
},
}
// Run tests...
}
Golden file tests for YAML parsing:
func TestParseCommands_GoldenFiles(t *testing.T) {
files, _ := filepath.Glob("testdata/*.yml")
for _, file := range files {
t.Run(filepath.Base(file), func(t *testing.T) {
data, _ := os.ReadFile(file)
_, err := ParseCommands(data)
if err != nil {
t.Errorf("failed to parse %s: %v", file, err)
}
})
}
}
Mocking External Commands:
// Use interfaces for testability
type TmuxClient interface {
ListSessions() ([]Session, error)
SwitchClient(name string) error
}
// Real implementation
type realTmuxClient struct{}
func (c *realTmuxClient) ListSessions() ([]Session, error) {
// Call actual tmux
}
// Mock for tests
type mockTmuxClient struct {
sessions []Session
err error
}
func (c *mockTmuxClient) ListSessions() ([]Session, error) {
return c.sessions, c.err
}
Error Handling¶
Return errors, don't panic:
// Good
func LoadConfig() (*Config, error) {
if _, err := os.Stat(configPath); err != nil {
return nil, fmt.Errorf("config not found: %w", err)
}
// ...
}
// Bad - only panic for programmer errors
func LoadConfig() *Config {
data := must(os.ReadFile(configPath)) // Don't do this
// ...
}
Wrap errors with context:
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("parse config file %s: %w", path, err)
}
Check errors explicitly:
// Good
sessions, err := client.ListSessions()
if err != nil {
return fmt.Errorf("list sessions: %w", err)
}
// Bad - ignoring errors
sessions, _ := client.ListSessions()
Custom error types when needed:
type SessionNotFoundError struct {
Name string
}
func (e *SessionNotFoundError) Error() string {
return fmt.Sprintf("session not found: %s", e.Name)
}
// Usage
if !exists {
return &SessionNotFoundError{Name: name}
}
// Checking
var notFound *SessionNotFoundError
if errors.As(err, ¬Found) {
// Handle specifically
}
Configuration¶
Use YAML for user-facing configs:
type Config struct {
Menu MenuConfig `yaml:"menu"`
Sessions SessionsConfig `yaml:"sessions"`
}
type MenuConfig struct {
Height int `yaml:"height"`
Preview bool `yaml:"preview_enabled"`
}
Validation:
func (c *Config) Validate() error {
if c.Menu.Height < 5 || c.Menu.Height > 50 {
return fmt.Errorf("menu height must be 5-50, got %d", c.Menu.Height)
}
return nil
}
func LoadConfig(path string) (*Config, error) {
var cfg Config
// ... parse YAML ...
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &cfg, nil
}
Default values:
func DefaultConfig() *Config {
return &Config{
Menu: MenuConfig{
Height: 20,
Preview: true,
},
}
}
func LoadConfig(path string) (*Config, error) {
cfg := DefaultConfig()
if _, err := os.Stat(path); os.IsNotExist(err) {
return cfg, nil // Use defaults
}
// ... load and merge ...
}
Logging¶
Use structured logging:
// Consider using log/slog (Go 1.21+)
import "log/slog"
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
logger.Info("creating session",
slog.String("name", name),
slog.String("directory", dir),
)
logger.Error("failed to create session",
slog.String("name", name),
slog.Any("error", err),
)
Log levels:
- Debug: Verbose details (disabled by default)
- Info: Important events (session created)
- Warn: Unexpected but handled (deprecated config)
- Error: Failures (can't create session)
No logs to stdout (reserve for output):
// Good - logs to stderr
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
// Bad - pollutes output
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
Performance¶
Startup time is critical:
- Target: <100ms for menu, <50ms for session manager
- Profile if slow:
go build -ldflags="-s -w"(strip debug info) - Lazy load when possible
Benchmarking:
func BenchmarkParseCommands(b *testing.B) {
data, _ := os.ReadFile("testdata/commands.yml")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = ParseCommands(data)
}
}
Optimize hot paths:
// Cache expensive operations
type Registry struct {
commands []Command
index map[string]*Command // Name lookup
}
func (r *Registry) FindByName(name string) (*Command, error) {
if cmd, ok := r.index[name]; ok {
return cmd, nil
}
return nil, ErrNotFound
}
CLI Design¶
Use cobra for consistency:
var rootCmd = &cobra.Command{
Use: "sess",
Short: "Fast tmux session manager",
Long: `A simple and fast tmux session manager built in Go.`,
}
var listCmd = &cobra.Command{
Use: "list",
Short: "List all sessions",
RunE: func(cmd *cobra.Command, args []string) error {
// Implementation
return nil
},
}
func init() {
rootCmd.AddCommand(listCmd)
}
Flags and arguments:
var (
flagAll bool
flagVerbose bool
)
func init() {
listCmd.Flags().BoolVarP(&flagAll, "all", "a", false, "Show all sessions")
listCmd.Flags().BoolVarP(&flagVerbose, "verbose", "v", false, "Verbose output")
}
Exit codes:
// 0 - Success
// 1 - General error
// 2 - Invalid arguments
// 3 - Config error
func main() {
if err := cmd.Execute(); err != nil {
if errors.Is(err, ErrInvalidConfig) {
os.Exit(3)
}
os.Exit(1)
}
}
TUI with Bubbletea¶
Model-Update-View pattern:
type model struct {
sessions []Session
cursor int
selected map[int]struct{}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "esc":
return m, tea.Quit
case "up", "k":
if m.cursor > 0 {
m.cursor--
}
case "down", "j":
if m.cursor < len(m.sessions)-1 {
m.cursor++
}
}
}
return m, nil
}
func (m model) View() string {
var s strings.Builder
s.WriteString("Sessions:\n\n")
for i, session := range m.sessions {
cursor := " "
if m.cursor == i {
cursor = ">"
}
fmt.Fprintf(&s, "%s %s\n", cursor, session.Name)
}
return s.String()
}
Testing TUIs:
func TestModel_Update(t *testing.T) {
m := model{
sessions: []Session{
{Name: "dotfiles"},
{Name: "notes"},
},
cursor: 0,
}
// Simulate down arrow
newModel, _ := m.Update(tea.KeyMsg{Type: tea.KeyDown})
m = newModel.(model)
if m.cursor != 1 {
t.Errorf("expected cursor=1, got %d", m.cursor)
}
}
Build & Release¶
Build flags:
# Development
go build -o sess .
# Production (smaller binary)
go build -ldflags="-s -w" -o sess .
# Static binary (no dependencies)
CGO_ENABLED=0 go build -ldflags="-s -w" -o sess .
Versioning:
Use debug.ReadBuildInfo() for automatic version detection from Go module info:
import "runtime/debug"
func getVersion() string {
if info, ok := debug.ReadBuildInfo(); ok {
if info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}
}
return "dev"
}
When installed via go install pkg@latest, the version automatically reflects git tags (e.g., v1.0.2). No ldflags needed.
App-level Task integration:
Each Go app has its own Taskfile.yml for local development:
# ~/tools/sess/Taskfile.yml
build:
desc: Build sess
vars:
VERSION:
sh: git describe --tags --always
COMMIT:
sh: git rev-parse --short HEAD
cmds:
- go build -ldflags="-s -w -X main.version={{.VERSION}} -X main.commit={{.COMMIT}}" -o sess ./cmd/sess
test:
desc: Run tests
cmds:
- go test -v ./...
For installation, use go install from GitHub rather than local builds.
Documentation¶
Package documentation:
// Package session provides tmux session management.
//
// It supports creating, listing, switching, and killing sessions.
// It integrates with both tmux and tmuxinator.
//
// Example:
//
// m := session.NewManager()
// sessions, err := m.ListSessions()
// if err != nil {
// log.Fatal(err)
// }
// for _, s := range sessions {
// fmt.Println(s.Name)
// }
package session
Function documentation:
// ListSessions returns all active tmux sessions.
//
// It returns an error if tmux is not running or if the
// tmux command fails.
func (m *Manager) ListSessions() ([]Session, error) {
// ...
}
README.md per tool:
# sess
Fast tmux session manager written in Go.
## Features
- List sessions
- Create sessions
- Switch sessions
- Tmuxinator integration
## Installation
\`\`\`bash
task go:install-session
\`\`\`
## Usage
\`\`\`bash
sess # Interactive menu
sess list # List sessions
sess create foo # Create session
\`\`\`
Common Patterns¶
Reading YAML files:
func LoadCommands(path string) (*CommandRegistry, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
var registry CommandRegistry
if err := yaml.Unmarshal(data, ®istry); err != nil {
return nil, fmt.Errorf("parse YAML: %w", err)
}
return ®istry, nil
}
Executing shell commands:
func listTmuxSessions() ([]string, error) {
cmd := exec.Command("tmux", "list-sessions", "-F", "#{session_name}")
output, err := cmd.Output()
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
return nil, fmt.Errorf("tmux command failed: %s", exitErr.Stderr)
}
return nil, fmt.Errorf("execute tmux: %w", err)
}
sessions := strings.Split(strings.TrimSpace(string(output)), "\n")
return sessions, nil
}
Checking if program exists:
Platform detection:
func detectPlatform() string {
switch runtime.GOOS {
case "darwin":
return "macos"
case "linux":
// Check for WSL
if _, err := os.Stat("/proc/version"); err == nil {
data, _ := os.ReadFile("/proc/version")
if strings.Contains(strings.ToLower(string(data)), "microsoft") {
return "wsl"
}
}
return "linux"
default:
return "unknown"
}
}
Pre-Commit Hooks¶
Go projects use comprehensive pre-commit hooks via tekwizely/pre-commit-golang:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.3.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [--strict]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-rc.1
hooks:
- id: go-fumpt-repo
- id: go-vet-repo-mod
- id: go-build-repo-mod
- id: go-mod-tidy-repo
- id: go-test-repo-mod
- id: golangci-lint-repo-mod
Install hooks:
pre-commit install
pre-commit install --hook-type commit-msg
pre-commit install --hook-type prepare-commit-msg
Configuration files (create in project root):
.golangci.yml- golangci-lint settings.markdownlint.json- markdownlint settings (disable MD013 for line length).editorconfig- editor formatting consistency