When to Use /internal vs /pkg Directory in Go Projects

Use /internal for private implementation details that the Go compiler actively prevents external modules from importing, and /pkg for stable public libraries intended for safe external consumption.

The golang-standards/project-layout repository defines the canonical structure for Go applications, establishing two distinct directories that control API visibility and module boundaries. Choosing between /internal and /pkg determines whether your code remains a private implementation detail or becomes part of your module's public contract with other projects.

Understanding the /internal Directory

According to internal/README.md in the golang-standards repository, the /internal directory contains private application and library code that must not be imported by other modules. The Go compiler enforces this restriction at the language level: packages inside an internal folder can only be imported by code that shares the same ancestor directory.

This compiler enforcement means that moving code into /internal guarantees third-party projects cannot accidentally depend on your implementation details. The directory is ideal for database adapters tuned to your internal schema, private configuration structures, or HTTP handlers that rely on internal assumptions.

Understanding the /pkg Directory

As documented in pkg/README.md, the /pkg directory holds library code that is safe for external applications to use. Unlike /internal, packages in /pkg are importable by any external module and represent a commitment to maintaining a stable API.

Code placed here should have clear documentation and a commitment to backward compatibility. Common examples include generic loggers, configuration parsers, or client libraries for external services that other projects may profit from.

When to Use /internal

Choose /internal when your code meets any of these criteria:

  • Implementation details subject to change: The package contains logic that could change without notice and should not be part of a public contract.
  • Tight application coupling: The code is specific to your application's internal architecture and isn't useful or safe for generic use.
  • Compiler-enforced privacy required: You want the Go compiler to actively prevent accidental imports from third-party projects.

For example, a set of database adapters tuned for your service's internal schema belongs in /internal because external projects should not depend on your specific data access patterns.

When to Use /pkg

Choose /pkg when your code meets these standards:

  • Stable, reusable API: The package offers functionality that other projects may legitimately need, such as a generic JSON utility or a standard logger.
  • Public commitment: You are prepared to maintain backward compatibility or provide clear migration paths for breaking changes.
  • Visual API boundary: You want the directory structure to serve as a clear signal that the code is publicly supported and versioned.

Practical Code Examples

Private Helper in /internal

The following code lives in internal/auth/token.go and generates tokens that should never be used outside the module:

// internal/auth/token.go
package auth // import path: example.com/project/internal/auth

import (
    "crypto/rand"
    "encoding/base64"
)

// NewToken generates a secret token used only inside the project.
func NewToken() (string, error) {
    b := make([]byte, 32)
    if _, err := rand.Read(b); err != nil {
        return "", err
    }
    return base64.StdEncoding.EncodeToString(b), nil
}

Usage within the same module is allowed:

// cmd/server/main.go
package main

import (
    "log"
    "example.com/project/internal/auth"
)

func main() {
    t, err := auth.NewToken()
    if err != nil {
        log.Fatal(err)
    }
    log.Println("internal token:", t)
}

A different module attempting to import example.com/project/internal/auth will receive a compiler error.

Public Library in /pkg

The following utility in pkg/jsonutil/pretty.go is safe for external consumption:

// pkg/jsonutil/pretty.go
package jsonutil // import path: example.com/project/pkg/jsonutil

import (
    "bytes"
    "encoding/json"
)

// Pretty returns an indented JSON representation of v.
func Pretty(v any) (string, error) {
    b, err := json.MarshalIndent(v, "", "  ")
    if err != nil {
        return "", err
    }
    return string(b), nil
}

External modules can safely import and use this package:

// another-module/main.go
package main

import (
    "log"
    "example.com/project/pkg/jsonutil"
)

func main() {
    s, _ := jsonutil.Pretty(map[string]string{"hello": "world"})
    log.Println(s)
}

Summary

  • /internal stores private code with compiler-enforced import restrictions, protecting implementation details from external dependencies.
  • /pkg stores public libraries with stable APIs intended for external consumption, requiring commitment to backward compatibility.
  • Both directories can coexist in a single project to separate public surface area from private logic.
  • The golang-standards/project-layout documentation in internal/README.md and pkg/README.md provides the authoritative definitions for these conventions.

Frequently Asked Questions

Can external packages import code from /internal?

No. The Go compiler strictly enforces that packages in /internal can only be imported by code within the same module tree that shares the same ancestor directory. Any external import attempt results in a compilation error, making this a stronger guarantee than documentation alone.

Is /pkg required for all public APIs?

While not strictly required by the Go compiler, /pkg is the community-standard convention indicating code intended for external use. Public APIs can technically reside elsewhere, but placing them in /pkg provides a clear organizational signal to other developers about which code is considered stable and supported.

Can a project use both /internal and /pkg together?

Yes. This is the recommended pattern: expose stable public libraries in /pkg while keeping all supporting logic, helper functions, and implementation details in /internal. This separation makes the architectural intent of each package explicit and leverages the compiler-enforced privacy of internal.

What happens if I move code from /pkg to /internal?

Moving code from /pkg to /internal is a breaking change for any external importers. The code immediately becomes inaccessible to external modules due to the compiler's import restrictions, effectively forcing external projects to find alternative implementations or fork the code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →