Skip to content
JavaGo
Part I · Orientation

Packages & Imports

A package is a directory, capitalisation is the only access modifier, and there are exactly two visibility scopes.

Section 01 of 31 3 min read

Every Go file begins with package name. All .go files in the same directory belong to one package, and the package name is the namespace callers use: short, lowercase, one word. There is no class wrapper: functions, types, variables and constants live directly at package level.

Package declaration and imports

Java — class per file, reverse-domain package
// File: UserService.java
package com.mycompany.service;

import com.mycompany.model.User;
import java.util.Optional;

public class UserService {
    // A class is required: no free functions
    public Optional<User> getUser(String id) {
        return Optional.empty();
    }
}
Go — package per directory, short name
// File: internal/service/user.go
package service

import (
    "errors"

    "github.com/mycompany/app/internal/model"
)

var ErrNotFound = errors.New("user not found")

// No class needed: functions live at package level
func GetUser(id string) (*model.User, error) {
    return nil, ErrNotFound
}
  • The import path is the module path plus the directory: github.com/mycompany/app/internal/model. The last element is conventionally the package name, so callers write model.User.
  • Standard-library imports come first, then a blank line, then everything else. gofmt and goimports maintain this for you.
  • An unused import is a compile error, not a warning. So is an unused local variable.
  • Import cycles are forbidden. If a imports b, b cannot import a. This keeps dependency graphs acyclic and builds fast, and it often forces a better package design.

Exported vs unexported: capitalisation is the access modifier

Java — keywords for visibility
public class User {
    public String id;          // public
    public String email;       // public
    private String password;   // private

    public static User newUser(String email) { ... }
    private String hashPw(String pw) { ... }
}
Go — first letter decides visibility
type User struct {
    ID       string // Uppercase: exported
    Email    string // Uppercase: exported
    password string // lowercase: package-private
}

// Exported function
func NewUser(email string) User { ... }

// Unexported function
func hashPw(pw string) string { ... }

internal/: visibility at the module level

Java 9 modules let you control which packages a module exports. Go has a lighter mechanism, enforced by the go command when it builds: a package under a directory named internal can only be imported by code rooted at the parent of that internal directory. Put everything that is not a deliberate public API under internal/ and outsiders cannot depend on it.

who may import an internal package
github.com/mycompany/app/
├── internal/
│   └── billing/        ← importable only from github.com/mycompany/app/...
├── cmd/api/main.go     ← can import app/internal/billing
└── go.mod

github.com/someone/else ← compile error: use of internal package not allowed

Capital letter: exported

internal/: importable from inside only

Fig. 1.1Two kinds of visibility. Between packages, the first letter of a name decides whether other packages can use it, for fields as well as functions. A package under internal/ goes further: only code rooted at the parent of that internal directory can import it.

Naming conventions

JavaGo
com.mycompany.handlers packagehandler: short, lowercase, singular, no underscores
UserServiceImplName the concrete type after what it is: PostgresStore, not StoreImpl
getUserId()UserID(): no Get prefix on getters; acronyms stay uppercase
StringUtils.isBlank()strings.TrimSpace(): the package name is part of the name
IReader / ReaderInterfaceReader: one-method interfaces end in -er
↑ ↓ to navigateEnter to openEsc to close