Go Web Tutorial 1

Build a simple RESTful API service with Go

Sun, 26 Aug 2018

sentry-banner

In this tutorial, we will go through how to build a simple RESTful APIs with Golang. The service will handle CRUD operatioins and response with simple json text.

Getting started

This is just to make sure the go environment setup is ready.

main.go

package main

import "fmt"

// handle all the http requests
func handleRequests() {
	// TODO: GET, POST, PUT, DELETE
}

func main() {
	fmt.Println("The server starts")
	handleRequests()
}
go run .\main.go
The server starts

Make a Http handler

package main

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

func index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Index page")
}

// handle all the http requests
func handleRequests() {
	// TODO: GET, POST, PUT, DELETE
	http.HandleFunc("/", index)

	log.Fatal(http.ListenAndServe("localhost:3000", nil))
}

func main() {
	fmt.Println("The server starts")
	handleRequests()
}

we added a http.HandlerFunc to map path / to our index function, which return simple words index page.

curl localhost:3000
Index page

Add a Router to handle request

First we have to import github.com/gorilla/mux, which is a very powerfull URL router for Golang.

import (
  // skips
  "github.com/gorilla/mux"
)

In our handleRequests() function, declare a new mux router using mux.NewRouter(), and replace http.HandleFunc() to router.HandleFunc(). Finally, Pass our mux router handler to http.ListenAndServe().

func handleRequests() {
	router := mux.NewRouter()

	// TODO: GET, POST, PUT, DELETE
	router.HandleFunc("/", index).Methods("GET")

	log.Fatal(http.ListenAndServe("localhost:3000", router))
}

Building GET, POST, PUT, DELETE endpoints

We make another file todos.go:

package main

import (
	"fmt"
	"net/http"
)

func ListTodos(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "list todos")
}

func AddTodo(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "add todo")
}

func UpdateTodo(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "update todo")
}

func DeleteTodo(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "delete todo")
}

Then, update our handleRequests() function to map the GET, POST, DELTE, PUT todos endpoints

func handleRequests() {
	router := mux.NewRouter()

	router.HandleFunc("/", index).Methods("GET")

	// GET, POST, PUT, DELETE
	router.HandleFunc("/todos", ListTodos).Methods("GET")
	router.HandleFunc("/todos", AddTodo).Methods("POST")
	router.HandleFunc("/todos", UpdateTodo).Methods("PUT")
	router.HandleFunc("/todos", DeleteTodo).Methods("DELETE")

	log.Fatal(http.ListenAndServe("localhost:3000", router))
}

Then run with go run .\main.go .\todos.go

Results:

curl localhost:3000/todos
list

curl localhost:3000/todos -X 'POST'
add todo

curl localhost:3000/todos -X 'PUT'
update

curl localhost:3000/todos -X 'DELETE'
delete

Build a ToDo model

in todos.go, add a Todo class, and declare variralbes to store data in memory

type Todo struct {
	ID      uint   `json:"id"`
	Title   string `json:"title"`
	Content string `json:"content"`
}

// in memory variables to act as db
var uuid uint = 1
var toDoStore []Todo = []Todo{}

Then, update our todo handler to return todo json.

import (
// skips others
	"encoding/json"
)
func ListTodos(w http.ResponseWriter, r *http.Request) {
	// fmt.Fprint(w, "list todos")
	json.NewEncoder(w).Encode(toDoStore)
}

func AddTodo(w http.ResponseWriter, r *http.Request) {
	// fmt.Fprint(w, "add todo")
	newTodo := Todo{ID: uuid, Title: "test", Content: "content"}
	toDoStore = append(toDoStore, newTodo)
	uuid++
	json.NewEncoder(w).Encode(newTodo)
}
curl localhost:3000/todos -X 'POST'
{"id":1,"title":"test","content":"content"}

curl localhost:3000/todos -X 'POST'
{"id":2,"title":"test","content":"content"}

curl localhost:3000/todos -X 'GET'
[{"id":1,"title":"test","content":"content"},{"id":2,"title":"test","content":"content"}]

Complete the CRUD operation

Update the handleRequests router

// GET, POST, PUT, DELETE
router.HandleFunc("/todos", ListTodos).Methods("GET")
router.HandleFunc("/todos", AddTodo).Methods("POST")
router.HandleFunc("/todos/{id:[0-9]+}", UpdateTodo).Methods("PUT")
router.HandleFunc("/todos/{id:[0-9]+}", DeleteTodo).Methods("DELETE")

In todos.go, add a helper function to parse the request json to Todo object

func decodeJSONRequest(r *http.Request) Todo {
	decoder := json.NewDecoder(r.Body)
	var rTodo Todo
	err := decoder.Decode(&rTodo)
	if err != nil {
		panic(err)
	}
	return rTodo
}

And update the CRUD operations:

func ListTodos(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(toDoStore)
}

func AddTodo(w http.ResponseWriter, r *http.Request) {
	rTodo := decodeJSONRequest(r)

	newTodo := Todo{ID: uuid, Title: rTodo.Title, Content: rTodo.Content}
	toDoStore = append(toDoStore, newTodo)
	uuid++
	json.NewEncoder(w).Encode(newTodo)
}

func UpdateTodo(w http.ResponseWriter, r *http.Request) {
	// Get id from url
	vars := mux.Vars(r)
	id, err := strconv.ParseUint(vars["id"], 10, 32)
	if err != nil {
		panic(err)
	}

	rTodo := decodeJSONRequest(r)

	var updatedIndex int
	for i, todo := range toDoStore {
		if todo.ID == uint(id) {
			newTodo := Todo{ID: todo.ID, Title: rTodo.Title, Content: rTodo.Content}
			toDoStore = append(toDoStore[:i], toDoStore[i+1:]...)
			toDoStore = append(toDoStore, newTodo)
			updatedIndex = i
			break
		}
	}

	json.NewEncoder(w).Encode(toDoStore[updatedIndex])
}

func DeleteTodo(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.ParseUint(vars["id"], 10, 32)
	if err != nil {
		panic(err)
	}
  
	var deletedTodo Todo
	for i, todo := range toDoStore {
		if todo.ID == uint(id) {
			toDoStore = append(toDoStore[:i], toDoStore[i+1:]...)
			deletedTodo = todo
			break
		}
	}
  
	json.NewEncoder(w).Encode(deletedTodo)
}

Results

go run main.go todos.go

curl http://localhost:3000/todos -X 'GET'
[]

# insert 3 dummy todos

curl http://localhost:3000/todos -X 'POST' -d '{"title": "test title1", "content": "c1"}'
{"id":1,"title":"test title1","content":"c1"}

curl http://localhost:3000/todos -X 'POST' -d '{"title": "test title2", "content": "c2"}'
{"id":2,"title":"test title2","content":"c2"}

curl http://localhost:3000/todos -X 'POST' -d '{"title": "test title3", "content": "c3"}'
{"id":3,"title":"test title3","content":"c3"}

curl http://localhost:3000/todos -X 'GET'
[{"id":1,"title":"test title1","content":"c1"},{"id":2,"title":"test title2","content":"c2"},{"id":3,"title":"test title3","content":"c3"}]

# Update Todo id = 3
curl http://localhost:3000/todos/3 -X 'PUT' -d '{"title": "updated3", "content": "update3"}'
{"id":3,"title":"updated3","content":"update3"}

curl http://localhost:3000/todos -X 'GET'
[{"id":1,"title":"test title1","content":"c1"},{"id":2,"title":"test title2","content":"c2"},{"id":3,"title":"updated3","content":"update3"}]

# Delete Todo id=2
curl http://localhost:3000/todos/2 -X 'DELETE'
{"id":2,"title":"test title2","content":"c2"}

curl http://localhost:3000/todos -X 'GET'
[{"id":1,"title":"test title1","content":"c1"},{"id":3,"title":"updated3","content":"update3"}]

This tutorial is only for demo basic Go CURD operations with in-memory storage. In later tutorial, we will discuss how to persist data in database using gorm.

Complete codes

main.go

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
)

func index(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "Index page")
}

// handle all the http requests
func handleRequests() {
	router := mux.NewRouter()

	router.HandleFunc("/", index).Methods("GET")

	// GET, POST, PUT, DELETE
	router.HandleFunc("/todos", ListTodos).Methods("GET")
	router.HandleFunc("/todos", AddTodo).Methods("POST")
	router.HandleFunc("/todos/{id:[0-9]+}", UpdateTodo).Methods("PUT")
	router.HandleFunc("/todos/{id:[0-9]+}", DeleteTodo).Methods("DELETE")

	log.Fatal(http.ListenAndServe("localhost:3000", router))
}

func main() {
	fmt.Println("The server starts")
	handleRequests()
}

todos.go

package main

import (
	"encoding/json"
	"net/http"
	"strconv"

	"github.com/gorilla/mux"
)

type Todo struct {
	ID      uint   `json:"id"`
	Title   string `json:"title"`
	Content string `json:"content"`
}

// in memory variables to act as db
var uuid uint = 1
var toDoStore []Todo = []Todo{}

func ListTodos(w http.ResponseWriter, r *http.Request) {
	json.NewEncoder(w).Encode(toDoStore)
}

func AddTodo(w http.ResponseWriter, r *http.Request) {
	rTodo := decodeJSONRequest(r)

	newTodo := Todo{ID: uuid, Title: rTodo.Title, Content: rTodo.Content}
	toDoStore = append(toDoStore, newTodo)
	uuid++
	json.NewEncoder(w).Encode(newTodo)
}

func UpdateTodo(w http.ResponseWriter, r *http.Request) {
	// Get id from url
	vars := mux.Vars(r)
	id, err := strconv.ParseUint(vars["id"], 10, 32)
	if err != nil {
		panic(err)
	}

	rTodo := decodeJSONRequest(r)

	var updatedIndex int
	for i, todo := range toDoStore {
		if todo.ID == uint(id) {
			newTodo := Todo{ID: todo.ID, Title: rTodo.Title, Content: rTodo.Content}
			toDoStore = append(toDoStore[:i], toDoStore[i+1:]...)
			toDoStore = append(toDoStore, newTodo)
			updatedIndex = i
			break
		}
	}

	json.NewEncoder(w).Encode(toDoStore[updatedIndex])
}

func DeleteTodo(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	id, err := strconv.ParseUint(vars["id"], 10, 32)
	if err != nil {
		panic(err)
	}

	var deletedTodo Todo
	for i, todo := range toDoStore {
		if todo.ID == uint(id) {
			toDoStore = append(toDoStore[:i], toDoStore[i+1:]...)
			deletedTodo = todo
			break
		}
	}

	json.NewEncoder(w).Encode(deletedTodo)
}

func decodeJSONRequest(r *http.Request) Todo {
	decoder := json.NewDecoder(r.Body)
	var rTodo Todo
	err := decoder.Decode(&rTodo)
	if err != nil {
		panic(err)
	}
	return rTodo
}
Loading...
Benny O

Benny O