Databases
One of the most asked questions I get about web development in Go is how to connect to a SQL database. Thankfully, Go has a fantastic SQL package in the standard library that allows us to use a whole slew of drivers for different SQL databases. In this example we will connect to a SQLite database, but the syntax (minus some small SQL semantics) is the same for a MySQL or PostgreSQL database.
Below is an example web application that has two endpoints: POST /books
to
create a book and GET /books
to show all books that have been created.
To create a new book, run:
curl -vX POST http://localhost:8080/books \
--header "Content-Type: application/json" \
-d "{\"author\": \"George Orwell\", \"title\": \"1984\"}"
To create another:
curl -vX POST http://localhost:8080/books \
--header "Content-Type: application/json" \
-d "{\"author\": \"Victor Hugo\", \"title\": \"Les Misérables\"}"
To view all books:
curl -v http://localhost:8080/books --header "Content-Type: application/json"
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
_ "github.com/mattn/go-sqlite3"
)
type book struct {
Author string `json:"author"`
Title string `json:"title"`
}
type errResponse struct {
Message string `json:"message"`
}
func main() {
db, err := newDB()
if err != nil {
log.Fatalln("Could not connect to database")
}
r := httprouter.New()
r.GET("/books", getHandler(db))
r.POST("/books", postHandler(db))
log.Println("Listening on :8080")
http.ListenAndServe(":8080", r)
}
func getHandler(db *sql.DB) httprouter.Handle {
return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
books, err := getBooks(db)
if err != nil {
respondError(rw, err)
return
}
rw.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(rw).Encode(books); err != nil {
respondError(rw, err)
return
}
}
}
func postHandler(db *sql.DB) httprouter.Handle {
return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) {
var b book
if err := json.NewDecoder(r.Body).Decode(&b); err != nil {
respondError(rw, err)
return
}
if err := createBook(db, b); err != nil {
respondError(rw, err)
return
}
rw.WriteHeader(http.StatusNoContent)
}
}
func newDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "example.sqlite")
if err != nil {
log.Println(err)
return nil, err
}
q := "CREATE TABLE IF NOT EXISTS books(title TEXT, author TEXT)"
if _, err := db.Exec(q); err != nil {
log.Println(err)
return nil, err
}
return db, nil
}
func getBooks(db *sql.DB) ([]book, error) {
q := "SELECT title, author FROM books"
rows, err := db.Query(q)
if err != nil {
log.Println(err)
return nil, err
}
var books []book
for rows.Next() {
var b book
if err := rows.Scan(&b.Title, &b.Author); err != nil {
log.Println(err)
return nil, err
}
books = append(books, b)
}
return books, nil
}
func createBook(db *sql.DB, b book) error {
q := "INSERT INTO books(title, author) VALUES ($1, $2)"
if _, err := db.Exec(q, b.Title, b.Author); err != nil {
log.Println(err)
return err
}
return nil
}
func respondError(rw http.ResponseWriter, err error) {
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusInternalServerError)
er := errResponse{
Message: err.Error(),
}
if err := json.NewEncoder(rw).Encode(er); err != nil {
log.Println(err)
}
}
Exercises
- Make use of the
Query
function on oursql.DB
instance to extract a collection of rows and map them to structs. - Add the ability to insert new records into our database by using an HTML form.
go get github.com/jmoiron/sqlx
and observe the improvements made over the existing database/sql package in the standard library.