You're reading a sample of this book. Get the full version here.
Let's Go Foundations › Project structure and organization
Previous · Contents · Next
Chapter 2.7.

Project structure and organization

Before we add any more code to our main.go file, it’s a good time to think how to organize and structure this project.

It’s important to explain upfront that there’s no single right — or even recommended — way to structure web applications in Go. And that’s both good and bad. It means that you have freedom and flexibility over how you organize your code, but it’s also easy to get stuck down a rabbit-hole of uncertainty when trying to decide what the best structure should be.

As you gain experience with Go, you’ll get a feel for which patterns work well for you in different situations. But as a starting point, the best advice I can give you is don’t over-complicate things. Try hard to add structure and complexity only when it’s demonstrably needed.

For this project, we’ll implement an outline structure that follows a popular and tried-and-tested approach. It’s a solid starting point, and you should be able to reuse the general structure in a wide variety of projects.

If you’re following along, make sure that you’re in the root of your project repository and run the following commands:

$ cd $HOME/code/snippetbox
$ rm main.go
$ mkdir -p cmd/web internal ui/html ui/static
$ touch cmd/web/main.go
$ touch cmd/web/handlers.go

The structure of your project repository should now look like this:

02.07-01.png

Let’s take a moment to discuss what each of these directories will be used for.

So why are we using this structure?

There are two big benefits:

  1. It gives a clean separation between Go and non-Go assets. All the Go code we write will live exclusively under the cmd and internal directories, leaving the project root free to hold non-Go assets like UI files, makefiles and module definitions (including our go.mod file).

  2. It scales really nicely if you want to add another executable application to your project. For example, you might want to add a CLI (Command Line Interface) to automate some administrative tasks in the future. With this structure, you could create this CLI application under cmd/cli and it will be able to import and reuse all the code you’ve written under the internal directory.

Refactoring your existing code

Let’s quickly port the code we’ve already written to this new structure.

File: cmd/web/main.go
package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /{$}", home)
    mux.HandleFunc("GET /snippet/view/{id}", snippetView)
    mux.HandleFunc("GET /snippet/create", snippetCreate)
    mux.HandleFunc("POST /snippet/create", snippetCreatePost)

    log.Print("starting server on :4000")
    
    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}
File: cmd/web/handlers.go
package main

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

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")
    w.Write([]byte("Hello from Snippetbox"))
}

func snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Display a form for creating a new snippet..."))
}

func snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusCreated)
    w.Write([]byte("Save a new snippet..."))
}

So now our web application consists of multiple .go files under the cmd/web directory. To run these, we can use the go run command like so:

$ cd $HOME/code/snippetbox
$ go run ./cmd/web
2024/03/18 11:29:23 starting server on :4000

Additional information

The internal directory

It’s important to point out that the directory name internal carries a special meaning and behavior in Go: any packages which live under this directory can only be imported by code inside the parent of the internal directory. In our case, this means that any packages which live in internal can only be imported by code inside our snippetbox project directory.

Or, looking at it the other way, this means that any packages under internal cannot be imported by code outside of our project.

This is useful because it prevents other codebases from importing and relying on the (potentially unversioned and unsupported) packages in our internal directory — even if the project code is publicly available somewhere like GitHub.