Beginner - Go

Writing REST Api with Go

REST APIs are everywhere. From mobile apps to web platforms, they connect the front-end and back-end in a clean and standard way. In this post, we’ll write a basic REST API using Go. It will not use any third-party packages. Only the Go standard library. We’ll simulate a database using a simple map. Go is fast, simple, and powerful. It compiles to a single binary. It starts up quickly and uses little memory. Go has strong support for concurrency, and the standard library is great for building APIs.

In a microservices setup, services talk to each other. Often they use REST for this communication.

Project Setup

Create a new directory for your app:

mkdir go-rest-api
cd go-rest-api

Initialize a Go Module. Run this inside the folder this will create a go.mod file.

go mod init go-rest-api

Create a file called main.go

touch main.go

Open main.go and add these imports

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strconv"
	"sync"
)

Define the Person Type

We’ll store and return Person objects through our API. A Person will have three fields: ID, Name, and Email. Let’s define it:

type Person struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Email string `json:"email"`
}

We’re not using a real database in this post. To keep things simple, we’ll use a map to store our data. The keys will be integers (IDs), and the values will be Person structs.

var (
	db        = make(map[int]Person)
	mu        sync.Mutex
	idCounter = 1
)

Why do we need the Mutex? Because the HTTP server may handle multiple requests at the same time. Without locking, the map can get corrupted. This setup is fine for small apps or demos. For real projects, you should use a proper database like PostgreSQL or MongoDB.

Implementing the Endpoints

Add Person

This endpoint reads a JSON object, gives it an ID, and stores it in our map.

func addPerson(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	var p Person
	err := json.NewDecoder(r.Body).Decode(&p)
	if err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	mu.Lock()
	p.ID = idCounter
	db[p.ID] = p
	idCounter++
	mu.Unlock()

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(p)
}

Get One Person

This reads an id from the query string and returns the matching person.

func getPerson(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	idStr := r.URL.Query().Get("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid ID", http.StatusBadRequest)
		return
	}

	mu.Lock()
	person, exists := db[id]
	mu.Unlock()

	if !exists {
		http.Error(w, "Person not found", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(person)
}

Get All People

This collects all people from the map and returns them as a JSON array.

func getAllPersons(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	mu.Lock()
	persons := make([]Person, 0, len(db))
	for _, p := range db {
		persons = append(persons, p)
	}
	mu.Unlock()

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(persons)
}

Update Person

This replaces a person in the map with new data.

func updatePerson(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPut {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	idStr := r.URL.Query().Get("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid ID", http.StatusBadRequest)
		return
	}

	var updated Person
	err = json.NewDecoder(r.Body).Decode(&updated)
	if err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	mu.Lock()
	_, exists := db[id]
	if !exists {
		mu.Unlock()
		http.Error(w, "Person not found", http.StatusNotFound)
		return
	}
	updated.ID = id
	db[id] = updated
	mu.Unlock()

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(updated)
}

Delete Person

This removes a person from the map by ID.

func deletePerson(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodDelete {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}

	idStr := r.URL.Query().Get("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		http.Error(w, "Invalid ID", http.StatusBadRequest)
		return
	}

	mu.Lock()
	_, exists := db[id]
	if exists {
		delete(db, id)
	}
	mu.Unlock()

	if !exists {
		http.Error(w, "Person not found", http.StatusNotFound)
		return
	}

	fmt.Fprintf(w, "Person with ID %d deleted\n", id)
}

Starting the Server

Now we need to connect each function to a URL path. We do this using http.HandleFunc(). Let’s put everything together inside main():

func main() {
	http.HandleFunc("/person", addPerson)
	http.HandleFunc("/person/get", getPerson)
	http.HandleFunc("/persons", getAllPersons)
	http.HandleFunc("/person/update", updatePerson)
	http.HandleFunc("/person/delete", deletePerson)

	fmt.Println("Server running at http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
  • This listens on port 8080.
  • Each route points to one of our functions.
  • We use log.Fatal() so we see any startup errors right away.

Now we’re ready to test the API.

Testing the API

You can test the API using curl, Postman, or any HTTP client. Here are some sample curl commands you can run in your terminal:

Add a New Person

curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com"}' \
http://localhost:8080/person

Get a Person by ID

curl http://localhost:8080/person/get?id=1

Get All People

curl http://localhost:8080/persons

Update a Person

curl -X PUT -H "Content-Type: application/json" \
-d '{"name":"Alice Smith","email":"alice.smith@example.com"}' \
"http://localhost:8080/person/update?id=1"

Delete a Person

curl -X DELETE "http://localhost:8080/person/delete?id=1"

Each response will come back as JSON (except for delete, which returns plain text).

Conclusion

We started with nothing, and now we have a working REST API written entirely in Go using only the standard library. It’s a simple project, but it covers a lot of ground. We defined a Person type to model our data, used a map as a basic in-memory database, and created endpoints to add, retrieve, update, and delete records. These are the core operations behind most backend services.

Of course, this isn’t production-ready code and it’s not meant to be. It’s a learning tool. It’s small on purpose, so we can focus on the essentials. Still, even this simple setup gives us a clear view into how Go handles HTTP, JSON, and basic data handling.

Suleyman Cabir Ataman, PhD

Sharing on social media:

Leave a Reply

Your email address will not be published. Required fields are marked *