Structuring Applications in Go

How I organize my applications in Go

Ben Johnson
Jul 6, 2014 · 7 min read

Overview

1. Don’t use global variables

package mainimport (
“fmt”
“net/http”
)
func main() {
http.HandleFunc(“/hello”, hello)
http.ListenAndServe(“:8080", nil)
}
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, “hi!”)
}
type HelloHandler struct {
db *sql.DB
}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var name string
// Execute the query.
row := h.db.QueryRow(“SELECT myname FROM mytable”)
if err := row.Scan(&name); err != nil {
http.Error(w, err.Error(), 500)
return
}
// Write it back to the client.
fmt.Fprintf(w, “hi %s!\n”, name)
}
func main() {
// Open our database connection.
db, err := sql.Open(“postgres”, “…”)
if err != nil {
log.Fatal(err)
}
// Register our handler.
http.Handle(“/hello”, &HelloHandler{db: db})
http.ListenAndServe(“:8080", nil)
}
func TestHelloHandler_ServeHTTP(t *testing.T) {
// Open our connection and setup our handler.
db, _ := sql.Open("postgres", "...")
defer db.Close()
h := HelloHandler{db: db}
// Execute our handler with a simple buffer.
rec := httptest.NewRecorder()
rec.Body = bytes.NewBuffer()
h.ServeHTTP(rec, nil)
if rec.Body.String() != "hi bob!\n" {
t.Errorf("unexpected response: %s", rec.Body.String())
}
}

2. Separate your binary from your application

camlistore/
cmd/
camget/
main.go
cammount/
main.go
camput/
main.go
camtool/
main.go

Library driven development

adder/
adder.go
cmd/
adder/
main.go
adder-server/
main.go
$ go get github.com/benbjohnson/adder/...

3. Wrap types for application-specific context

package myappimport (
"database/sql"
)
type DB struct {
*sql.DB
}
type Tx struct {
*sql.Tx
}
// Open returns a DB reference for a data source.
func Open(dataSourceName string) (*DB, error) {
db, err := sql.Open("postgres", dataSourceName)
if err != nil {
return nil, err
}
return &DB{db}, nil
}
// Begin starts an returns a new transaction.
func (db *DB) Begin() (*Tx, error) {
tx, err := db.DB.Begin()
if err != nil {
return nil, err
}
return &Tx{tx}, nil
}
// CreateUser creates a new user.
// Returns an error if user is invalid or the tx fails.
func (tx *Tx) CreateUser(u *User) error {
// Validate the input.
if u == nil {
return errors.New("user required")
} else if u.Name == "" {
return errors.New("name required")
}

// Perform the actual insert and return any errors.
return tx.Exec(`INSERT INTO users (...) VALUES`, ...)
}

Transactional composition

tx, _ := db.Begin()
tx.CreateUser(&User{Name:"susy"})
tx.Commit()
tx, _ := db.Begin()
for _, u := range users {
tx.CreateUser(u)
}
tx.Commit()

4. Don’t go crazy with subpackages

bucket.go
cursor.go
db.go
freelist.go
node.go
page.go
tx.go

Conclusion

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store