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

> Discover the typical Go project directory layout including cmd internal and pkg folders. Learn best practices from Openship's structure for efficient Go development.

- Repository: [oblien/openship](https://github.com/oblien/openship)
- Tags: best-practices
- Published: 2026-07-31

---

**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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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.

```go
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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/cmd/openship/main.go). This entry point remains thin, delegating logic to packages while handling only process initialization and error handling.

```go
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`](https://github.com/oblien/openship/blob/main/internal/server/server.go), preventing external coupling while allowing multiple internal packages to share implementation details.

```go
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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/main.go) file be located in a Go project?

Place [`main.go`](https://github.com/oblien/openship/blob/main/main.go) files inside subdirectories of `cmd/` (e.g., [`cmd/server/main.go`](https://github.com/oblien/openship/blob/main/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`](https://github.com/oblien/openship/blob/main/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.