← Blog

Go Programming Language — Packages

2020-01-24 · 5 min read

goprogramming

Go Programming Language — Packages

Go Programming Language

In Go, code organization is achieved through packages. A package represents the files inside a directory on disk. Both the libraries you use within your code and your application’s main entry point are packages.

Naming

Package names are expected — but not required — to match the name of the directory containing them.

A package declaration goes at the top of every source file using the package keyword. Every source file must have a package declaration:

package sendMail

import {
  ...
}

If a package is located at:

$GOPATH/src/foo/bar

You import it like this:

import "foo/bar"

To import a package hosted on GitHub under a repository named foo:

import "github.com/nailcankucuk/foo/bar"

When using an imported package, you typically access its functions through the last path element:

package main

import "github.com/nailcankucuk/foo/bar"

func main() {
  bar.sayHello()
}

Executable Packages

An executable program must have a source file declaring package main and containing a main() function. The main() function is defined exactly once, takes no parameters, and has no return type. It is the program’s entry point:

package main

func main() {
  fmt.Println("Hello!")
}

Scope and Visibility

Go has no public, private, or protected keywords. Instead, visibility is determined by the case of the first letter of an identifier:

  • An uppercase first letter means the identifier is exported (accessible from other packages).
  • A lowercase first letter means the identifier is unexported (package-private).
// Accessible from other packages
func Foo() {}

// Only accessible within this package
var bar string

You cannot directly access or assign unexported fields of a struct defined in another package:

package main

import (
  "fmt"
  "github.com/nailcankucuk/userops"
)

func main() {
  user := userops.NewUser("Nail", "Küçük", "12345")
  fmt.Printf("%+v\n", user)
  // Expected output: {First:Nail Last:Küçük password:12345}

  // Direct access to unexported field is not allowed:
  fmt.Println(user.password)
  // ./main.go:17:18: user.password undefined (cannot refer to unexported field or method password)

  //user.password = "new"
  // ./main.go:18:6: user.password undefined (cannot refer to unexported field or method password)
}

As a best practice, avoid using unexported types as the return type of an exported function. The code will compile, but it is semantically incorrect:

type bar struct {}

func Foo() bar {
  return bar{}
}

Package Organization

When starting to write code, it is not always obvious how to structure your packages. Many approaches exist, but none is universally best. Often the relationships and design patterns only become clear as the codebase matures — so one valid strategy is to write the code first and refactor into packages once it reaches a certain level of maturity.

One critical constraint to keep in mind: Go does not allow circular package imports.

  • If package A imports package B, then package B must not import package A.
  • If A imports B, B imports C, and C imports A, this also forms a cycle.

Organizing packages strictly by domain module can easily lead to circular references. For example, separating users and department into their own packages may eventually require each to import the other, creating a cycle.

A recommended approach is to organize packages into four categories:

  • Domain Package
  • Implementation Packages
  • Mock Package
  • Binary Packages

Domain Package

The domain package contains the application-specific types at the core of the project. It may include:

  • Data models (structs): Account, User
  • Service interfaces: AccountService, UserService
  • Utility functions: SortUsers(), UserCache

Implementation Packages

Implementation packages are named after the technologies they use, and contain the concrete implementations of domain interfaces.

For example, CRUD operations on a Foo struct using PostgreSQL would live in a postgres package, while Redis-backed caching for the same struct would live in a redis package. This isolates implementations by technology and enables independent testing:

demo/
└── foo.go
└── postgres
│   └── foo.go
└── redis
    └── foo.go

Mock Package

Test doubles (mocks) for all services are collected in a single mock package:

demo/
└── mock
    └── foo.go

Binary Packages

The compiled binaries for each executable in the project live under a cmd directory, with each binary in its own subdirectory:

demo/
└── cmd
    ├── demoserver
    │   └── main.go
    └── democlient
        └── main.go

A complete example project structure:

demo/
├── cmd
│   ├── demoserver
│   │   └── main.go
│   └── democlient
│       └── main.go
├── mock
│   └── foo.go
├── postgres
│   └── foo.go
├── redis
│   └── foo.go
└── foo.go

For large projects that span multiple domains, split the code into separate packages per domain. Common types and interfaces shared across domains can live at the root level. The benefits:

  • Enforces clean communication between layers through interfaces.
  • Each package can be tested independently.
  • Prevents circular dependencies.
  • Keeps the codebase maintainable as it grows — issues stay contained within their package.

Installing and Updating External Packages

Like most languages, Go allows you to use packages written by others. To install an external package, use the go get command. Packages are downloaded into the directory defined by the GOPATH environment variable:

$ go env GOPATH
/Users/nailcankucuk/go

To download a package:

$ go get github.com/nailcankucuk/foo

This places the package at:

$GOPATH/src/github.com/nailcankucuk/foo

You can then import and use it in your code.

To update a package to its latest version (or download it if it doesn’t exist locally):

$ go get -u github.com/nailcankucuk/foo