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.
| Unit | Description | Closest .NET equivalent |
|---|---|---|
| Package | The set of .go files in one directory. The unit of compilation and visibility | namespace + a folder in a project |
| Module | A collection of packages with a go.mod. The unit of versioning and distribution | Assembly (csproj) / NuGet package |
2. .NET Mapping
| .NET (C#) | Go |
|---|---|
| sln / csproj | go.work / go.mod |
| namespace / assembly | package / module |
public / internal / private | Uppercase = exported / lowercase = unexported / internal/ directory |
| NuGet | Go modules (go get / proxy.golang.org) |
PackageReference | require in go.mod |
packages.lock.json | go.sum (hash-based tamper detection) |
dotnet add package | go get |
dotnet restore | go mod download (runs automatically on build) |
dotnet list package --outdated | go list -u -m all |
ProjectReference | replace 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
)
| Directive | Meaning |
|---|---|
module | The module path. It becomes the prefix of import paths. For public modules it is usually the repository URL |
go | The minimum required Go version |
require | Dependency modules and versions |
replace | Redirects a dependency to another path or a local directory |
exclude | Prevents a specific version from being used |
toolchain | Specifies 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
.gofiles in a directory share the samepackagedeclaration (except_testpackages for tests) - Unlike C#, you cannot give each file a different namespace
- By convention the package name matches the directory name (
internal/orderispackage order) - The entry point of an executable is
func main()inpackage 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.
| Identifier | Visibility | Closest C# equivalent |
|---|---|---|
Order / NewService / Total | Accessible from other packages (exported) | public |
order / newService / total | Within 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 ininternal/ - C#
internalis scoped to the assembly; Go'sinternal/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, orhelpers. Choose names that read well with the package name, such asorder.Service(order.OrderServiceis 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 -uor Dependabot / Renovate - The
goline ingo.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 commitgo.modandgo.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.workfor multi-module development andGOPRIVATEfor private modules.
Next, see Basic Syntax.