Backend Development 8 min read

Guide to Organizing Go Project Structure, Packages, and Commands

This article explains how to design a Go project layout, differentiate internal and external packages, configure modules, write simple functions, import packages, create command‑line tools, and manage private code, providing practical code examples and best‑practice recommendations for backend developers.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Guide to Organizing Go Project Structure, Packages, and Commands

Go has become one of the most popular languages for cloud development, and organizing a Go project’s directory structure is a common challenge. This guide walks through project layout, the distinction between internal and external packages, and how to structure command‑line tools.

Key questions addressed:

How does the project structure reflect import paths?

How to organize command‑line tools alongside code?

How to flexibly arrange code across modules?

How can multiple packages coexist in a single module?

How do multiple packages live together in one module?

Terminology: An internal package is private to the module and cannot be imported by external code.

Example project layout (modlib):

├── LICENSE
├── README.md
├── config.go
├── go.mod
├── go.sum
├── clientlib
│   ├── lib.go
│   └── lib_test.go
├── cmd
│   ├── modlib-client
│   │   └── main.go
│   └── modlib-server
│       └── main.go
├── internal
│   └── auth
│       ├── auth.go
│       └── auth_test.go
└── serverlib
    └── lib.go

The go.mod file defines the module name (e.g., module github.com/qidian/modlib ) and, in this example, has no dependencies.

Simple function example (config.go):

package modlib

func Config() string {
    return "modlib config"
}

Importing this module from another package looks like:

package main

import "fmt"
import "github.com/qidian/modlib"

func main() {
    fmt.Println(modlib.Config())
}

The clientlib package provides its own function:

package clientlib

func Hello() string {
    return "clientlib hello"
}

Using both modlib and clientlib together:

package main

import "fmt"
import "github.com/qidian/modlib"
import "github.com/qidian/modlib/clientlib"

func main() {
    fmt.Println(modlib.Config())
    fmt.Println(clientlib.Hello())
}

The cmd directory holds executable commands. After installing with go get github.com/qidian/modlib/cmd/modlib-client , the binary can be run directly.

$ go get github.com/qidian/modlib/cmd/modlib-client
$ modlib-client
Running client
Config: modlib config
clientlib hello

Similarly, the server command demonstrates importing multiple packages, including an internal package:

package main

import (
    "fmt"
    "github.com/qidian/modlib"
    "github.com/qidian/modlib/internal/auth"
    "github.com/qidian/modlib/serverlib"
)

func main() {
    fmt.Println("Running server")
    fmt.Println("Config:", modlib.Config())
    fmt.Println("Auth:", auth.GetAuth())
    fmt.Println(serverlib.Hello())
}

Attempting to import an internal package from outside the module results in an error such as:

use of internal package github.com/eliben/modlib/internal/auth not allowed

Private packages are useful for encapsulating implementation details that should not be part of the public API, especially when adhering to semantic versioning.

Overall, a clear project layout helps developers quickly locate code, understand module boundaries, and maintain a clean separation between public and private components.

backend developmentGomodulesInternal PackagesProject Layout
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.