Typical Directory Layout for a Go Project Like Openship: Structure & Best Practices

A typical Go project layout places executables in cmd/, private libraries in internal/, public libraries in pkg/, and module files at the root, as demonstrated by the minimal HTTP server in Openship's fixtures/deploy/go/ directory.

Understanding the typical directory layout for a Go project helps maintain clean separation of concerns and ensures compatibility with Go's module system. While the openship repository primarily focuses on deployment automation, its fixtures/deploy/go/ path contains a canonical example of Go project organization that follows official Go conventions and community standards.

Standard Go Directory Conventions

Production-grade Go repositories organize code into specific directories that signal visibility and purpose to both developers and the compiler.

cmd/

The cmd/ directory contains entry points for executable binaries. Each subdirectory represents a separate application, with its own main.go file that bootstraps the service. This pattern keeps binary-specific code isolated from reusable libraries.

internal/

Packages placed under internal/ enforce private visibility—the Go compiler prohibits external modules from importing these packages. This directory houses implementation details that should remain within the repository boundary.

pkg/

The pkg/ directory holds public libraries intended for consumption by external projects. Unlike internal/, code here exposes a stable API that other modules can import and depend upon.

Module Files

Every Go project requires go.mod and go.sum at the repository root. These files declare the module name (e.g., github.com/oblien/openship) and lock dependency versions.

Openship's Current Go Implementation

The openship repository currently maintains a demonstration service at fixtures/deploy/go/main.go. This single-file implementation illustrates the foundational pattern for a standalone Go service before scaling to a multi-package architecture.

The Fixture Entry Point

Located at fixtures/deploy/go/main.go, the existing code implements a minimal HTTP server that follows twelve-factor app principles by accepting configuration through environment variables.

package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}
	http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintln(w, "hello from go")
	})
	http.ListenAndServe(":"+port, nil)
}

This example demonstrates the canonical single-file structure: a dedicated folder containing one main.go that reads environment variables (defaulting PORT to 8080) and binds an HTTP handler to the root path.

Evolving Toward Production Layout

When expanding beyond the fixture stage, openship would migrate from the single-file prototype to the standard directory hierarchy. This evolution separates concerns while maintaining the configuration-via-environment pattern established in the original implementation.

Command Directory Structure

Production binaries reside under cmd/openship/main.go. This entry point remains thin, delegating logic to packages while handling only process initialization and error handling.

package main

import (
	"log"
	"os"

	"github.com/oblien/openship/internal/server"
)

func main() {
	if err := server.Start(os.Getenv("PORT")); err != nil {
		log.Fatalf("failed to start server: %v", err)
	}
}

Internal Package Organization

Core business logic moves to internal/server/server.go, preventing external coupling while allowing multiple internal packages to share implementation details.

package server

import (
	"fmt"
	"net/http"
)

// Start runs the HTTP server on the given port.
func Start(port string) error {
	if port == "" {
		port = "8080"
	}
	http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
		fmt.Fprintln(w, "Welcome to Openship!")
	})
	return http.ListenAndServe(":"+port, nil)
}

Key Files in the Repository

  • fixtures/deploy/go/main.go – The current demonstration server showing basic HTTP handling and environment-based configuration.
  • go.mod (future) – Would declare github.com/oblien/openship as the module path at the repository root.
  • cmd/openship/main.go (future) – Would serve as the production binary entry point following standard layout conventions.
  • internal/ (future) – Would contain private implementation packages leveraging Go's internal visibility enforcement.

Summary

  • Standard Go projects use cmd/ for binaries, internal/ for private code, and pkg/ for public libraries.
  • Openship's current Go code lives in fixtures/deploy/go/main.go, demonstrating a minimal HTTP server configured via the PORT environment variable.
  • Future expansion would migrate this logic to cmd/openship/main.go and internal/server/ while maintaining the environment-driven configuration pattern.
  • Module files (go.mod and go.sum) belong at the repository root to manage dependencies for the entire project.

Frequently Asked Questions

What is the purpose of the internal/ directory in Go projects?

The internal/ directory enforces compiler-level privacy. Any package placed under this directory cannot be imported by code outside the module, ensuring implementation details remain encapsulated and preventing accidental API exposure to external consumers.

Where should the main.go file be located in a Go project?

Place main.go files inside subdirectories of cmd/ (e.g., cmd/server/main.go) when building multiple binaries, or at the repository root only for single-file utilities. This convention clearly separates executable commands from reusable library code.

What is the difference between pkg/ and internal/ directories?

pkg/ contains public libraries that external projects can import and depend upon, while internal/ contains private implementation details restricted to the current repository. Use pkg/ for stable, exported APIs and internal/ for code that should not become a public dependency.

How does Openship handle configuration in its Go example?

The fixtures/deploy/go/main.go file reads the PORT environment variable using os.Getenv(), defaulting to 8080 if unset. This follows the twelve-factor methodology, keeping configuration separate from code and enabling portability across different deployment environments.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →