Skip to main content

Modules and Packages

For developers with .NET (C#) experience, this guide covers go.mod, packages, visibility rules, internal packages, and dependency management.

1. How Modules and Packages Relate​

Go organizes code with two units.

UnitDescriptionClosest .NET equivalent
PackageThe set of .go files in one directory. The unit of compilation and visibilitynamespace + a folder in a project
ModuleA collection of packages with a go.mod. The unit of versioning and distributionAssembly (csproj) / NuGet package

2. .NET Mapping​

.NET (C#)Go
sln / csprojgo.work / go.mod
namespace / assemblypackage / module
public / internal / privateUppercase = exported / lowercase = unexported / internal/ directory
NuGetGo modules (go get / proxy.golang.org)
PackageReferencerequire in go.mod
packages.lock.jsongo.sum (hash-based tamper detection)
dotnet add packagego get
dotnet restorego mod download (runs automatically on build)
dotnet list package --outdatedgo list -u -m all
ProjectReferencereplace directive / go.work
using Foo.Bar;import "example.com/foo/bar"

3. The Role of go.mod and go.sum​

Creating a module​

mkdir shop && cd shop
go mod init github.com/example/shop

The generated go.mod:

module github.com/example/shop

go 1.23

require (
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.1
)
DirectiveMeaning
moduleThe module path. It becomes the prefix of import paths. For public modules it is usually the repository URL
goThe minimum required Go version
requireDependency modules and versions
replaceRedirects a dependency to another path or a local directory
excludePrevents a specific version from being used
toolchainSpecifies the Go toolchain to use

go.sum​

go.sum records cryptographic hashes of dependency modules and verifies that downloaded content has not been tampered with. Always commit it along with go.mod. It is not meant to be edited by hand.

Minimal Version Selection (MVS)​

NuGet may pick "the newest within an allowed range". Go instead picks the minimum version that satisfies the require constraints (Minimal Version Selection). Dependencies never change unless you update them explicitly, which makes builds highly reproducible.

Working with dependencies​

go get github.com/google/uuid # add (latest)
go get github.com/google/[email protected] # pin a version
go get -u ./... # update dependencies (minor/patch)
go mod tidy # remove unused, add missing
go mod download # fetch dependencies into the cache
go mod vendor # copy into the vendor/ directory
go mod why github.com/google/uuid # explain why it is needed
go list -m all # list dependency modules

go mod tidy reconciles go.mod / go.sum with the import statements in your source. Run it whenever you change dependencies to keep diffs clean.

4. Packages and Directories​

One directory = one package​

  • All .go files in a directory share the same package declaration (except _test packages for tests)
  • Unlike C#, you cannot give each file a different namespace
  • By convention the package name matches the directory name (internal/order is package order)
  • The entry point of an executable is func main() in package main
shop/
├── go.mod
├── cmd/
│ └── api/
│ └── main.go // package main
├── internal/
│ └── order/
│ ├── order.go // package order
│ ├── service.go // package order
│ └── service_test.go // package order (or order_test)
└── pkg/
└── money/
└── money.go // package money

import​

import (
"context"
"fmt"

"github.com/google/uuid"

"github.com/example/shop/internal/order"
)
  • The path is the module path + the directory within the module
  • You reference it by package name: order.NewService(...)
  • Group imports as standard library, third-party, then your own (formatted by goimports / gofmt)
  • Unused imports are compile errors

Aliases and blank imports​

import (
str "strings" // alias
_ "github.com/lib/pq" // side effects only (runs init())
. "fmt" // dot import (discouraged)
)

Use a blank import (_) when the side effect is the whole point, such as registering a database/sql driver.

The init function​

Each package may define func init() (even multiple per file). It runs automatically on import, but it adds implicit behavior, so use it sparingly. It is similar to a C# static constructor.

5. Visibility Rules​

Go has no access modifiers (public / private). Visibility is determined by whether the first letter of an identifier is uppercase.

IdentifierVisibilityClosest C# equivalent
Order / NewService / TotalAccessible from other packages (exported)public
order / newService / totalWithin the package only (unexported)Narrower than internal (package-private)
package order

// Order is a type exposed to other packages
type Order struct {
ID string // exported field
Total int64 // exported field
status string // unexported field
}

// NewOrder is an exported function
func NewOrder(id string) *Order {
return &Order{ID: id, status: "new"}
}

// validate is an unexported function (callable only inside this package)
func validate(o *Order) error { return nil }

Notes:

  • The unit of visibility is the package, not the type. Code in the same package can freely access unexported fields
  • There is no equivalent of protected (there is no inheritance)
  • Reflection-based libraries such as JSON serializers can only see exported fields (and json:"name" tags only make sense on exported fields)

6. The internal Directory​

Packages under a directory named internal can only be imported from within the parent directory's tree. The compiler enforces this.

github.com/example/shop/
├── internal/order/ ← importable only by code under shop
└── pkg/money/ ← importable from other modules too
// From an external module (github.com/other/app):
import "github.com/example/shop/internal/order"
// → use of internal package ... not allowed
  • For applications, putting code under internal/ by default is the safe choice (it minimizes the public API surface)
  • For libraries, expose only the intended API at the root or in pkg/, and hide implementation details in internal/
  • C# internal is scoped to the assembly; Go's internal/ is scoped to a directory tree

7. Workspaces and replace​

Developing multiple modules together (go.work)​

go work init ./app ./lib
go 1.23

use (
./app
./lib
)

Like a ProjectReference, this lets you reference unpublished local modules. go.work is usually not committed (it is for your personal environment).

The replace directive​

replace github.com/example/lib => ../lib

This lives in go.mod and affects everyone on the team. Use it for temporary debugging or for forks, and do not leave it in permanently.

8. Private Modules and vendor​

Private repositories​

Internal company modules cannot be fetched through the public proxy (proxy.golang.org). Exclude them with an environment variable.

go env -w GOPRIVATE=github.com/my-company/*

Authentication uses your Git configuration (SSH keys or credential helpers).

vendor​

go mod vendor
go build -mod=vendor ./...

This copies dependency sources into vendor/. It is used for offline builds and auditing, but it is usually unnecessary (the module cache and proxy already ensure reproducibility).

9. Common Pitfalls for C# Developers​

  • One directory = one package: you cannot split namespaces freely. File names do not affect compilation
  • Circular imports are forbidden: if package A imports B and B imports A, compilation fails. You must design the dependency direction (for example, define interfaces on the consumer side)
  • Case determines visibility: a common bug is a lowercase field name that never shows up in JSON
  • Package names should be short, lowercase, and singular: avoid generic names like utils, common, or helpers. Choose names that read well with the package name, such as order.Service (order.OrderService is redundant)
  • The module path is part of every import path: changing it later means rewriting all imports, so decide it up front
  • Major version 2 and above appears in the path: for example github.com/jackc/pgx/v5. Different major versions are distinct modules and can coexist
  • Dependencies are not updated automatically: update deliberately with go get -u or Dependabot / Renovate
  • The go line in go.mod: it indicates the minimum language version. If your toolchain is older, a newer one is downloaded automatically (Go 1.21+)

10. Code Example​

C#​

// Shop.Orders.csproj / namespace Shop.Orders
namespace Shop.Orders;

public class OrderService
{
private readonly IOrderRepository _repo;

public OrderService(IOrderRepository repo) => _repo = repo;

public Task<Order?> FindAsync(string id) => _repo.FindAsync(id);

internal void Validate(Order order) { /* ... */ }
}

Go​

// internal/order/service.go
package order

import "context"

// Repository is defined on the consumer side (the order package)
type Repository interface {
Find(ctx context.Context, id string) (*Order, error)
}

// Service is an exported type
type Service struct {
repo Repository // unexported field
}

// NewService is an exported constructor function
func NewService(repo Repository) *Service {
return &Service{repo: repo}
}

// Find is an exported method
func (s *Service) Find(ctx context.Context, id string) (*Order, error) {
return s.repo.Find(ctx, id)
}

// validate is an unexported method
func (s *Service) validate(o *Order) error {
return nil
}
// cmd/api/main.go
package main

import (
"context"
"fmt"
"log"

"github.com/example/shop/internal/order"
)

func main() {
svc := order.NewService(nil) // pass a real repository implementation
o, err := svc.Find(context.Background(), "1")
if err != nil {
log.Fatal(err)
}
fmt.Println(o)
}

11. Summary​

  • A module (go.mod) is the unit of versioning; a package (a directory) is the unit of compilation and visibility.
  • Manage dependencies with go get / go mod tidy, and always commit go.mod and go.sum. Thanks to MVS, dependencies do not change unless you ask.
  • Visibility is decided by whether an identifier starts with an uppercase or lowercase letter. The unit is the package, not the class.
  • internal/ is a compiler-enforced private area. Application implementation code belongs there by default.
  • Packages cannot import each other circularly. Use short lowercase names and avoid generic ones like utils.
  • Use go.work for multi-module development and GOPRIVATE for private modules.

Next, see Basic Syntax.

References​