Structuring Applications in Go

How I organize my applications in Go

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

Writing databases and distributed systems in Go.

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