返回 Skills
cxuu/golang-skills· Apache-2.0 内容可用

go-packages

Use when creating Go packages, organizing imports, managing dependencies, or deciding how to structure Go code into packages. Also use when starting a new Go project or splitting a growing codebase into packages, even if the user doesn't explicitly ask about package organization. Does not cover naming individual identifiers (see go-naming).

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: go-packages description: Use when creating Go packages, organizing imports, managing dependencies, or deciding how to structure Go code into packages. Also use when starting a new Go project or splitting a growing codebase into packages, even if the user doesn't explicitly ask about package organization. Does not cover naming individual identifiers (see go-naming).

Go Packages and Imports

Resource Routing

  • references/IMPORTS.md - Read when grouping imports, using blank imports, dot imports, or import aliases.
  • references/PACKAGE-SIZE.md - Read when splitting packages, avoiding init, structuring main, or designing CLI flags/subcommands.

When this skill does NOT apply: For naming individual identifiers within a package, see go-naming. For organizing functions within a single file, see go-functions. For configuring linters that enforce import rules, see go-linting.

Package Organization

Avoid Util Packages

Package names should describe what the package provides. Avoid generic names like util, helper, common — they obscure meaning and cause import conflicts.

// Good: Meaningful package names
db := spannertest.NewDatabaseFromFile(...)
_, err := f.Seek(0, io.SeekStart)

// Bad: Vague names obscure meaning
db := test.NewDatabaseFromFile(...)
_, err := f.Seek(0, common.SeekStart)

Generic names can be used as part of a name (e.g., stringutil) but should not be the entire package name.

Package Size

QuestionAction
Can you describe its purpose in one sentence?No → split by responsibility
Do files never share unexported symbols?Those files could be separate packages
Distinct user groups use different parts?Split along user boundaries
Godoc page overwhelming?Split to improve discoverability

Do NOT split just because a file is long, to create single-type packages, or if it would create circular dependencies.


Imports

Imports are organized in groups separated by blank lines. Standard library packages always come first. Use goimports to manage this automatically.

import (
    "fmt"
    "os"

    "github.com/foo/bar"
    "rsc.io/goversion/version"
)

Quick rules:

RuleGuidance
Groupingstdlib first, then external. Extended: stdlib → other → protos → side-effects
RenamingAvoid unless collision. Rename the most local import. Proto packages get pb suffix
Blank imports (import _)Only in main packages or tests
Dot imports (import .)Never use, except for circular-dependency test files

Avoid init()

Avoid init() where possible. When unavoidable, it must be:

  1. Completely deterministic
  2. Independent of other init() ordering
  3. Free of environment state (env vars, working dir, args)
  4. Free of I/O (filesystem, network, system calls)

Acceptable uses: complex expressions that can't be single assignments, pluggable hooks (e.g., database/sql dialects), deterministic precomputation.


Exit in Main

Call os.Exit or log.Fatal* only in main(). All other functions should return errors.

Why: Non-obvious control flow, untestable, defer statements skipped.

Best practice: Use the run() pattern — extract logic into func run() error, call from main() with a single exit point:

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

Command-Line Flags

Advisory: Define flags only in package main.

  • Flag names use snake_case: --output_dir not --outputDir
  • Libraries should accept configuration as parameters, not read flags directly — this keeps them testable and reusable
  • Prefer the standard flag package; use pflag only when POSIX conventions (double-dash, single-char shortcuts) are required
// Good: Flag in main, passed as parameter to library
func main() {
    outputDir := flag.String("output_dir", ".", "directory for output files")
    flag.Parse()
    if err := mylib.Generate(*outputDir); err != nil {
        log.Fatal(err)
    }
}

Related Skills

  • Package naming: See go-naming when choosing package names, avoiding stuttering, or naming exported symbols
  • Error handling across packages: See go-error-handling when wrapping errors at package boundaries with %w vs %v
  • Import linting: See go-linting when configuring goimports local-prefixes or enforcing import grouping
  • Global state: See go-defensive when replacing init() with explicit initialization or avoiding mutable globals

附带文件

references/IMPORTS.md
# Import Organization

Detailed rules and examples for organizing Go imports.

## Import Grouping

Imports are organized in groups, with blank lines between them. The standard
library packages are always in the first group.

**Minimal grouping (Uber):** stdlib, then everything else.

**Extended grouping (Google):** stdlib → other → protocol buffers → side-effects.

```go
// Good: Standard library separate from external packages
import (
    "fmt"
    "os"

    "go.uber.org/atomic"
    "golang.org/x/sync/errgroup"
)
```

```go
// Good: Full grouping with protos and side-effects
import (
    "fmt"
    "os"

    "github.com/dsnet/compress/flate"
    "golang.org/x/text/encoding"

    foopb "myproj/foo/proto/proto"

    _ "myproj/rpc/protocols/dial"
)
```

## Import Renaming

Avoid renaming imports except to avoid a name collision; good package names
should not require renaming. In the event of collision, **prefer to rename the
most local or project-specific import**.

**Must rename:** collision with other imports, generated protocol buffer packages
(remove underscores, add `pb` suffix).

**May rename:** uninformative names (e.g., `v1`), collision with local variable.

```go
// Good: Proto packages renamed with pb suffix
import (
    foosvcpb "path/to/package/foo_service_go_proto"
)

// Good: urlpkg when url variable is needed
import (
    urlpkg "net/url"
)

func parseEndpoint(url string) (*urlpkg.URL, error) {
    return urlpkg.Parse(url)
}
```

## Blank Imports (`import _`)

Packages that are imported only for their side effects (using `import _ "pkg"`)
should only be imported in the main package of a program, or in tests that
require them.

```go
// Good: Blank import in main package
package main

import (
    _ "time/tzdata"
    _ "image/jpeg"
)
```

## Dot Imports (`import .`)

**Do not** use dot imports. They make programs much harder to read because it is
unclear whether a name like `Quux` is a top-level identifier in the current
package or in an imported package.

**Exception:** The `import .` form can be useful in tests that, due to circular
dependencies, cannot be made part of the package being tested:

```go
package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)
```

In this case, the test file cannot be in package `foo` because it uses
`bar/testutil`, which imports `foo`. So the `import .` form lets the file
pretend to be part of package `foo` even though it is not.

**Except for this one case, do not use `import .` in your programs.**

```go
// Bad: Dot import hides origin
import . "foo"
var myThing = Bar() // Where does Bar come from?

// Good: Explicit qualification
import "foo"
var myThing = foo.Bar()
```
references/PACKAGE-SIZE.md
# Package Size, Program Structure, and CLIs

Detailed guidance on package splitting, avoiding init(), the run() pattern, and
CLI structure.

## Contents

- [When to Split a Package](#when-to-split-a-package)
- [Avoiding init()](#avoiding-init)
- [Exit in Main](#exit-in-main)
- [Command-Line Interfaces](#command-line-interfaces)

## When to Split a Package

```
Is the package getting too large?
├─ Can you describe its purpose in one sentence?
│  ├─ No → Split by responsibility
│  └─ Yes → Keep it, but check below
├─ Do files in the package never import each other's unexported symbols?
│  └─ Yes → Those files could be separate packages
├─ Does the package have distinct user groups using different parts?
│  └─ Yes → Split along user boundaries
└─ Is the godoc page overwhelming?
   └─ Yes → Split to improve discoverability
```

### When NOT to Split

- Don't split just because a file is long — large files in a focused package are
  fine
- Don't create packages with only one type or function
- Don't split if it would create circular dependencies
- Avoid splitting internal helpers into a `util` or `internal/helpers` package

### When to Combine Packages

- If client code likely needs two types to interact, keep them together
- If types have tightly coupled implementations
- If users would need to import both packages to use either meaningfully

### File Organization

No "one type, one file" convention in Go. Files should be focused enough to know
which file contains something and small enough to find things easily.

---

## Avoiding init()

Prefer explicit functions over `init()`:

```go
// Bad: init() with I/O and environment dependencies
var _config Config

func init() {
    cwd, _ := os.Getwd()
    raw, _ := os.ReadFile(path.Join(cwd, "config.yaml"))
    yaml.Unmarshal(raw, &_config)
}
```

```go
// Good: Explicit function for loading config
func loadConfig() (Config, error) {
    cwd, err := os.Getwd()
    if err != nil {
        return Config{}, err
    }

    raw, err := os.ReadFile(path.Join(cwd, "config.yaml"))
    if err != nil {
        return Config{}, err
    }

    var config Config
    if err := yaml.Unmarshal(raw, &config); err != nil {
        return Config{}, err
    }
    return config, nil
}
```

**Acceptable uses of init():**
- Complex expressions that cannot be single assignments
- Pluggable hooks (e.g., `database/sql` dialects, encoding registries)
- Deterministic precomputation

---

## Exit in Main

Call `os.Exit` or `log.Fatal*` **only in `main()`**. All other functions should
return errors to signal failure.

**Why this matters:**
- Non-obvious control flow: Any function can exit the program
- Difficult to test: Functions that exit also exit the test
- Skipped cleanup: `defer` statements are skipped

```go
// Bad: log.Fatal in helper function
func readFile(path string) string {
    f, err := os.Open(path)
    if err != nil {
        log.Fatal(err)  // Exits program, skips defers
    }
    b, err := io.ReadAll(f)
    if err != nil {
        log.Fatal(err)
    }
    return string(b)
}
```

```go
// Good: Return errors, let main() decide to exit
func main() {
    body, err := readFile(path)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(body)
}

func readFile(path string) (string, error) {
    f, err := os.Open(path)
    if err != nil {
        return "", err
    }
    b, err := io.ReadAll(f)
    if err != nil {
        return "", err
    }
    return string(b), nil
}
```

### The run() Pattern

Prefer to call `os.Exit` or `log.Fatal` **at most once** in `main()`. Extract
business logic into a separate function that returns errors.

```go
func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    args := os.Args[1:]
    if len(args) != 1 {
        return errors.New("missing file")
    }

    f, err := os.Open(args[0])
    if err != nil {
        return err
    }
    defer f.Close()  // Will always run

    b, err := io.ReadAll(f)
    if err != nil {
        return err
    }

    // Process b...
    return nil
}
```

**Benefits of the `run()` pattern:**
- Short `main()` function with single exit point
- All business logic is testable
- `defer` statements always execute

---

## Command-Line Interfaces

### Flag Naming

Use lowercase, hyphen-separated flag names:

```go
// Good
flag.String("output-dir", ".", "directory for output files")
flag.Bool("dry-run", false, "print actions without executing")

// Bad
flag.String("outputDir", ".", "")    // camelCase
flag.String("output_dir", ".", "")   // underscores
```

### Subcommands

For complex CLIs with subcommands, use `flag.NewFlagSet` per subcommand:

```go
func main() {
    serveCmd := flag.NewFlagSet("serve", flag.ExitOnError)
    port := serveCmd.Int("port", 8080, "listen port")

    migrateCmd := flag.NewFlagSet("migrate", flag.ExitOnError)
    dryRun := migrateCmd.Bool("dry-run", false, "preview changes")

    switch os.Args[1] {
    case "serve":
        serveCmd.Parse(os.Args[2:])
        runServe(*port)
    case "migrate":
        migrateCmd.Parse(os.Args[2:])
        runMigrate(*dryRun)
    default:
        fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1])
        os.Exit(1)
    }
}
```

For larger CLIs, consider libraries like `cobra` or `urfave/cli`. Exit only from
`main()`.