How to Create Your Own Package in Go

Initialize a Go module with go mod init and write your code in a .go file to create a package.

The copy-paste trap

You wrote a function to parse a weird log format. It works. You copy-paste it into three different files. Then you find a bug where the parser chokes on a timestamp with a timezone offset. Now you have to fix it in three places, and you're pretty sure you missed one. Or you're building a small library to share with a friend, and you're stuck on how Go expects you to organize files versus how Python does it with __init__.py or how JavaScript handles index.js.

Go doesn't care about file names. It cares about packages and modules. The mental model shifts from "files contain code" to "directories contain packages, and repositories contain modules." Once you accept that shift, the tooling stops fighting you. You get a system where imports are unambiguous, visibility is enforced by capitalization, and the compiler catches structural mistakes before you run anything.

Modules and packages

Go has two layers of organization. A module is a collection of packages, usually in one repository, tracked by version control. The module has a go.mod file that declares the module path. The module path is the address the world uses to import your code. It looks like a URL, but it doesn't have to be hosted on the web. It's just a unique string that identifies the module.

A package is a collection of .go files that live in the same directory and share the same package declaration. Every file in a package directory must start with package name. The package name usually matches the directory name. This is not a suggestion. The tooling expects it. If the directory is mypkg, the files should declare package mypkg.

Think of a module like a book. The module path is the ISBN. A package is a chapter. You cite the book to get the chapter, but the book keeps everything together. You can import a chapter directly, but the book defines the version and the metadata.

Minimal example

Here's the skeleton: a module with one custom package and a main program that uses it. The package lives in a subdirectory, and the main program imports it using the module path.

# Create the module root
mkdir mylib
cd mylib

# Initialize the module with a version-control-style path
# The path is the import root for everything inside this directory
go mod init github.com/user/mylib

Create a directory for the package. The directory name becomes part of the import path.

mkdir mathutil

Write the package code. Every file in mathutil/ declares package mathutil. Public names start with a capital letter. Private names start lowercase. The compiler enforces visibility, not keywords.

// mathutil/add.go
package mathutil

// Add returns the sum of two integers.
// It is exported because the name starts with a capital letter.
func Add(a, b int) int {
    return a + b
}

// addHelper is private to this package.
// Other packages cannot call this function.
func addHelper(x, y int) int {
    return x + y
}

You can split a package across multiple files. All files in the directory compile together and share the same namespace. A function in one file can call a function in another file directly, without imports.

// mathutil/multiply.go
package mathutil

// Multiply returns the product of two integers.
// It calls addHelper to demonstrate intra-package visibility.
func Multiply(a, b int) int {
    result := 0
    for i := 0; i < b; i++ {
        result = addHelper(result, a)
    }
    return result
}

Write the main program. It imports the package using the full module path plus the directory name.

// main.go
package main

import (
    "fmt"
    // Import the package using the module path plus directory
    "github.com/user/mylib/mathutil"
)

func main() {
    // Call the exported function
    sum := mathutil.Add(2, 3)
    fmt.Println("Sum:", sum)

    // Multiply is also exported
    product := mathutil.Multiply(4, 5)
    fmt.Println("Product:", product)
}

Run the program. The compiler resolves the import, compiles the package, and links it into the executable.

go run main.go
# Output:
# Sum: 5
# Product: 20

Package names match directory names. The compiler enforces this. If they differ, you get a warning and the import alias becomes awkward.

How the compiler resolves your code

When you run go build or go run, the compiler reads the go.mod file in the current directory or any parent directory. It finds the module path. It maps imports to directories relative to the module root.

If you import github.com/user/mylib/mathutil, the compiler looks for a directory named mathutil inside the module. It reads all .go files in that directory. It checks that every file declares package mathutil. If a file declares a different package name, the compiler rejects the program with an error like found packages mathutil and other in /path/to/dir.

The compiler also checks visibility. If you try to call mathutil.addHelper from main.go, the compiler rejects the program with mathutil.addHelper undefined (cannot refer to unexported name or method). This is how Go controls access. No public or private keywords. Just capitalization.

If you forget to use an import, the compiler rejects the program with imported and not used. Go is strict about unused imports. This keeps dependencies clean. If you don't need it, remove it.

Trust gofmt. Argue logic, not formatting. Most editors run gofmt on save. The tool decides indentation, spacing, and import grouping. Don't fight it.

Realistic package structure

A real package usually defines types, methods, and handles errors. Here's a package that loads configuration. It uses a struct, a method with a receiver, and returns an error.

// config/config.go
package config

import (
    "fmt"
    "os"
)

// Config holds application settings.
// Fields are exported so callers can read them.
type Config struct {
    Port int
    Name string
}

// Load reads configuration from a path.
// It returns the Config and an error if loading fails.
func Load(path string) (Config, error) {
    // Check if the file exists
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap the error to provide context
        return Config{}, fmt.Errorf("load config: %w", err)
    }

    // Parse the data (simplified for this example)
    // In real code, you might use json.Unmarshal or yaml.Unmarshal
    var c Config
    if len(data) == 0 {
        return Config{}, fmt.Errorf("config file is empty")
    }

    c.Port = 8080
    c.Name = string(data)
    return c, nil
}

The receiver name is usually one or two letters matching the type. (c *Config) is standard. (this *Config) or (self *Config) is not Go style.

Methods attach to types. A method can be defined in any file within the package, as long as the type is defined in the same package.

// config/validate.go
package config

import "fmt"

// Validate checks if the configuration is valid.
// The receiver is a pointer so the method can modify the struct if needed,
// though this method only reads.
func (c *Config) Validate() error {
    if c.Port < 1 || c.Port > 65535 {
        return fmt.Errorf("invalid port: %d", c.Port)
    }
    if c.Name == "" {
        return fmt.Errorf("name cannot be empty")
    }
    return nil
}

The main program uses the package. It handles errors explicitly. The if err != nil pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible.

// main.go
package main

import (
    "fmt"
    "log"
    "github.com/user/mylib/config"
)

func main() {
    // Load the config
    cfg, err := config.Load("config.txt")
    if err != nil {
        // Log the error and exit
        log.Fatal(err)
    }

    // Validate the config
    if err := cfg.Validate(); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Running %s on port %d\n", cfg.Name, cfg.Port)
}

Accept interfaces, return structs. This is the most common Go style mantra. Your package returns a concrete Config struct. Callers can use it directly. If you need abstraction, define an interface in the package that uses the config, not in the package that defines it.

Pitfalls and compiler errors

Creating a package seems simple, but a few traps catch beginners.

Directory name mismatch. If your directory is mathutil but your files declare package math, the compiler warns with import path mismatch: imported directory "mathutil" as "github.com/user/mylib/math". The import path uses the directory name, not the package declaration. Rename the package declaration to match the directory, or use an import alias. Matching them is the cleanest approach.

Module path vs local path. The module path in go.mod should be a version-control path, not a local filesystem path. If you set the module path to /home/user/mylib, imports will break when you share the code. Use a path like github.com/user/mylib or example.com/mylib. The path doesn't have to be hosted; it just needs to be unique and stable.

main package constraints. Only one package in a program can be named main, and it must contain a main function. If you put package main in a library package, you can't import it. The compiler rejects the program with main: redefinition of main. Library packages must use a non-main name.

Unused variables. If you assign a value and don't use it, the compiler rejects the program with declared and not used. Use the underscore _ to discard a value intentionally. result, _ := ... says "I considered the second return value and chose to drop it". Use it sparingly with errors. Discarding an error silently is usually a bug waiting to happen.

Goroutine leaks. If your package spawns goroutines, ensure they can exit. A goroutine that waits on a channel that never gets closed will leak. Always have a cancellation path, usually via context.Context. Context is plumbing. Run it through every long-lived call site. The worst goroutine bug is the one that never logs.

The compiler catches structural errors. Runtime panics come from nil dereferences or channel misuse. Write tests. Test packages live in the same directory as the code, with filenames ending in _test.go. They declare package name to test internal details, or package name_test to test the public API.

When to split and when to stay

Go encourages small, focused packages. But splitting too early adds friction. Use this decision matrix to guide your structure.

Use a new package when you have a distinct domain concept that needs its own namespace and visibility rules. Use a single package when your code is small enough that splitting it adds friction without improving clarity. Use a module when you are grouping related packages into a versioned unit for distribution or dependency management. Use the main package only for the entry point of an executable. Use a test package when you want to test the public API without access to internal helpers. Use an internal package when you need to hide code from external consumers but share it within the module.

Don't create a package for every function. A package should group related types and functions. If a package has only one exported function and no types, consider merging it into a larger package. If a package has fifty exported functions and no clear theme, consider splitting it.

Modules are the address. Packages are the content. Keep the module path stable. Rename packages freely during development, but update imports everywhere. The tooling helps with go fix, but consistency matters.

Public names start with a capital letter. Private names start lowercase. The compiler enforces visibility. Don't pass a *string. Strings are already cheap to pass by value. Don't fight the type system. Wrap the value or change the design.

Where to go next