
Prasyarat
- Go versi 1.18 atau lebih baru sudah terinstal
- Pemahaman dasar bahasa Go
- Tools:
curlatau Postman untuk testing
Cek instalasi Go:
go version Struktur Project
go-rest-api/
βββ main.go
βββ handler/
β βββ user_handler.go
βββ model/
β βββ user.go
βββ repository/
β βββ user_repository.go
βββ router/
β βββ router.go
βββ go.mod Struktur ini mengikuti prinsip **Separation of Concerns** β setiap file punya satu tanggung jawab yang jelas. Kalau nanti kamu ingin mengganti penyimpanan in-memory ke database PostgreSQL misalnya, kamu hanya perlu mengubah file `repository` tanpa menyentuh handler atau router sama sekali.
| Komponen | Tanggung Jawab |
|---|---|
main.go | Titik masuk program, rakit semua komponen, nyalakan server |
router/ | Arahkan request HTTP ke handler yang tepat berdasarkan URL |
handler/ | Proses request, validasi input, buat response |
repository/ | Simpan, cari, ubah, dan hapus data |
model/ | Definisikan bentuk/struktur data |
Langkah 1 β Inisialisasi Project
mkdir go-rest-api
cd go-rest-api
go mod init go-rest-api mkdir membuat folder baru, cd masuk ke dalamnya. Perintah go mod init go-rest-api adalah yang paling penting β ini membuat file go.mod yang memberitahu Go bahwa folder ini adalah sebuah project (disebut βmoduleβ). Isinya seperti identitas kartu nama project kamu. Tanpa ini, Go tidak tahu cara menghubungkan antar file dalam project.
Langkah 2 β Buat Model
Buat file model/user.go:
package model
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
} struct adalah cara Go mendefinisikan βbentukβ sebuah data, mirip seperti template formulir. Di sini kita mendefinisikan bahwa setiap User punya 3 kolom: ID (angka), Name (teks), dan Email (teks).
Bagian `json:"id"` disebut struct tag. Ini memberitahu Go cara menerjemahkan data ke format JSON. Jadi ketika kamu kirim response ke client, field ID akan muncul sebagai "id" (huruf kecil), bukan "ID". Ini penting karena konvensi JSON menggunakan huruf kecil, sedangkan Go menggunakan huruf besar untuk nama field yang bisa diakses dari luar package (exported field).
Langkah 3 β Buat Repository (Penyimpanan In-Memory)
Buat file repository/user_repository.go:
package repository
import (
"errors"
"go-rest-api/model"
"sync"
)
type UserRepository struct {
mu sync.RWMutex
users map[int]model.User
counter int
}
func NewUserRepository() *UserRepository {
return &UserRepository{
users: make(map[int]model.User),
}
}
func (r *UserRepository) FindAll() []model.User {
r.mu.RLock()
defer r.mu.RUnlock()
list := make([]model.User, 0, len(r.users))
for _, u := range r.users {
list = append(list, u)
}
return list
}
func (r *UserRepository) FindByID(id int) (model.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
u, ok := r.users[id]
if !ok {
return model.User{}, errors.New("user not found")
}
return u, nil
}
func (r *UserRepository) Create(u model.User) model.User {
r.mu.Lock()
defer r.mu.Unlock()
r.counter++
u.ID = r.counter
r.users[u.ID] = u
return u
}
func (r *UserRepository) Update(id int, u model.User) (model.User, error) {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.users[id]; !ok {
return model.User{}, errors.New("user not found")
}
u.ID = id
r.users[id] = u
return u, nil
}
func (r *UserRepository) Delete(id int) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.users[id]; !ok {
return errors.New("user not found")
}
delete(r.users, id)
return nil
} Repository adalah lapisan penyimpanan data. Analoginya seperti laci arsip β semua operasi simpan, cari, ubah, hapus data dilakukan di sini. Handler tidak boleh langsung menyentuh data mentah; semua harus lewat repository. Ini membuat kode lebih terorganisir dan mudah diubah di kemudian hari.
Mari kita bedah bagian-bagian pentingnya:
type UserRepository struct {
mu sync.RWMutex
users map[int]model.User
counter int
} usersadalah in-memory database β data disimpan di RAM (bukan di file atau database sungguhan). Konsekuensinya, data akan hilang saat server dimatikan.counteradalah auto-increment ID, mirip seperti primary key di database. Setiap kali user baru dibuat, counter bertambah 1.sync.RWMutexadalah kunci pengaman untuk concurrent access. Bayangkan banyak orang mengakses API bersamaan β tanpa kunci ini, bisa terjadi race condition (dua request menulis data di waktu yang sama, hasilnya bisa rusak atau hilang).RWMutexmembolehkan banyak goroutine membaca sekaligus (RLock), tapi hanya satu goroutine yang boleh menulis (Lock).
Lima fungsi di bawahnya mewakili operasi dasar database yang disebut CRUD: Create (buat), Read (baca β FindAll & FindByID), Update (ubah), Delete (hapus).
Perhatikan pola defer r.mu.Unlock() β kata kunci defer memastikan kunci selalu dilepas setelah fungsi selesai, bahkan jika terjadi error di tengah jalan. Ini mencegah deadlock (kondisi di mana semua goroutine saling menunggu kunci yang tidak pernah dilepas).
Langkah 4 β Buat Handler
Buat file handler/user_handler.go:
package handler
import (
"encoding/json"
"go-rest-api/model"
"go-rest-api/repository"
"net/http"
"strconv"
"strings"
)
type UserHandler struct {
repo *repository.UserRepository
}
func NewUserHandler(repo *repository.UserRepository) *UserHandler {
return &UserHandler{repo: repo}
}
// Helper: tulis JSON response
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Helper: tulis error response
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
// Helper: ambil ID dari path (misal: /users/3 β 3)
func extractID(path string) (int, error) {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) < 2 {
return 0, strconv.ErrSyntax
}
return strconv.Atoi(parts[len(parts)-1])
}
// GET /users
func (h *UserHandler) GetAll(w http.ResponseWriter, r *http.Request) {
users := h.repo.FindAll()
writeJSON(w, http.StatusOK, users)
}
// GET /users/{id}
func (h *UserHandler) GetByID(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r.URL.Path)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user id")
return
}
user, err := h.repo.FindByID(id)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeJSON(w, http.StatusOK, user)
}
// POST /users
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var u model.User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if u.Name == "" || u.Email == "" {
writeError(w, http.StatusBadRequest, "name and email are required")
return
}
created := h.repo.Create(u)
writeJSON(w, http.StatusCreated, created)
}
// PUT /users/{id}
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r.URL.Path)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user id")
return
}
var u model.User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
updated, err := h.repo.Update(id, u)
if err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeJSON(w, http.StatusOK, updated)
}
// DELETE /users/{id}
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := extractID(r.URL.Path)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid user id")
return
}
if err := h.repo.Delete(id); err != nil {
writeError(w, http.StatusNotFound, err.Error())
return
}
writeJSON(w, http.StatusOK, map[string]string{"message": "user deleted"})
} Handler adalah penerima request HTTP. Ketika ada request masuk, handler yang memutuskan: apakah data valid? Apa yang harus dilakukan? Apa yang harus dikembalikan ke client? Analoginya seperti kasir di restoran β menerima pesanan (request), meneruskan ke dapur (repository), lalu membawa makanan kembali ke meja (response).
Mari kita bedah tiga fungsi helper-nya:
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
} Fungsi ini mengirim response dalam format JSON. w.Header().Set("Content-Type", "application/json") memberitahu client bahwa isi response adalah JSON, bukan HTML atau teks biasa. w.WriteHeader(status) mengirim HTTP status code (misalnya 200 OK atau 404 Not Found). Fungsi ini dibuat agar tidak perlu menulis ulang kode yang sama di setiap handler.
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
} Shortcut khusus untuk mengirim response error. Misalnya memanggil writeError(w, 404, "user not found") akan menghasilkan response {"error": "user not found"} dengan status 404.
func extractID(path string) (int, error) {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) < 2 {
return 0, strconv.ErrSyntax
}
return strconv.Atoi(parts[len(parts)-1])
} Mengambil angka ID dari URL. Misalnya dari /users/3, fungsi ini memotong string berdasarkan tanda / menjadi slice ["users", "3"], lalu mengambil elemen terakhir "3" dan mengubahnya menjadi angka integer 3.
Langkah 5 β Buat Router
Buat file router/router.go:
package router
import (
"go-rest-api/handler"
"net/http"
"strings"
)
func NewRouter(userHandler *handler.UserHandler) http.Handler {
mux := http.NewServeMux()
// Route: /users dan /users/{id}
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
userHandler.GetAll(w, r)
case http.MethodPost:
userHandler.Create(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
// Pastikan ada ID setelah /users/
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
if len(parts) < 2 || parts[1] == "" {
http.NotFound(w, r)
return
}
switch r.Method {
case http.MethodGet:
userHandler.GetByID(w, r)
case http.MethodPut:
userHandler.Update(w, r)
case http.MethodDelete:
userHandler.Delete(w, r)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
return mux
} Router adalah peta jalan untuk request HTTP. Ketika ada request masuk ke server, router yang memutuskan handler mana yang akan dipanggil berdasarkan URL dan HTTP method-nya.
http.NewServeMux() adalah multiplexer bawaan Go β ini yang mencocokkan URL request dengan handler yang tepat. Kita mendaftarkan dua pola URL:
/usersβ menangani request tanpa ID (GET semua user, POST buat user baru)/users/β menangani request dengan ID di belakangnya (GET, PUT, DELETE by ID). Tanda/di akhir penting karena di Go, pola yang diakhiri/akan mencocokkan semua URL yang diawali dengan pola tersebut.
Satu URL bisa menangani banyak method HTTP. Router memeriksa r.Method untuk menentukan tindakan yang tepat. GET /users artinya βambil semua userβ, tapi POST /users artinya βbuat user baruβ β URL sama, tindakan berbeda. Ini adalah konvensi standar REST API.
Langkah 6 β Buat Main Entry Point
Buat file main.go:
package main
import (
"fmt"
"go-rest-api/handler"
"go-rest-api/repository"
"go-rest-api/router"
"log"
"net/http"
)
func main() {
// Inisialisasi dependency
userRepo := repository.NewUserRepository()
userHandler := handler.NewUserHandler(userRepo)
mux := router.NewRouter(userHandler)
port := ":8080"
fmt.Printf("Server berjalan di http://localhost%s\n", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatalf("Gagal menjalankan server: %v", err)
}
} main.go adalah titik masuk program β kode yang pertama kali dijalankan Go. Di sini semua bagian dirakit menjadi satu, seperti memasang semua komponen mesin sebelum menyalakannya.
userRepo := repository.NewUserRepository() // buat "database" in-memory
userHandler := handler.NewUserHandler(userRepo) // buat handler, kasih akses ke database
mux := router.NewRouter(userHandler) // buat router, kasih akses ke handler Pola ini disebut dependency injection β setiap komponen βdisuntikβ dengan komponen yang dibutuhkannya. Handler butuh repository untuk mengakses data, maka repository dikirim ke handler saat pembuatan. Keuntungannya: kode lebih mudah diuji karena kita bisa mengganti implementasi tanpa mengubah logika handler.
http.ListenAndServe(":8080", mux) Perintah ini artinya: βdengarkan koneksi di port 8080, dan gunakan mux untuk memproses semua request yang masuk.β Port 8080 adalah konvensi umum untuk server development (port 80 adalah HTTP standar, tapi butuh hak akses administrator).
Langkah 7 β Jalankan Server
go run main.go go run mengompilasi dan langsung menjalankan program dalam satu perintah β cocok untuk development. Untuk produksi, gunakan go build terlebih dahulu untuk menghasilkan file executable, lalu jalankan file tersebut.
Output:
Server berjalan di http://localhost:8080 Langkah 8 β Testing dengan curl
Setelah server berjalan, buka terminal baru dan coba endpoint berikut. Ini adalah alur lengkap yang menggambarkan siklus hidup data dari create hingga delete.
Buat User (POST)
curl -X POST http://localhost:8080/users
-H "Content-Type: application/json"
-d '{"name": "Budi Santoso", "email": "budi@email.com"}' Flag -X POST menentukan HTTP method. Flag -H menambahkan header request. Flag -d mengirim data di body request. Server akan membalas dengan user yang baru dibuat, lengkap dengan ID yang di-generate otomatis.
Response:
{"id": 1, "name": "Budi Santoso", "email": "budi@email.com"} Ambil Semua User (GET)
curl http://localhost:8080/users Request GET paling sederhana β tanpa flag tambahan karena GET adalah method default curl. Response berupa array JSON, bahkan jika hanya ada satu user.
Response:
[{"id": 1, "name": "Budi Santoso", "email": "budi@email.com"}] Ambil User Berdasarkan ID (GET)
curl http://localhost:8080/users/1 Angka 1 di akhir URL adalah ID user. Kalau ID tidak ditemukan, server akan membalas dengan 404 Not Found dan pesan error.
Update User (PUT)
curl -X PUT http://localhost:8080/users/1
-H "Content-Type: application/json"
-d '{"name": "Budi Updated", "email": "budi.new@email.com"}' PUT menggantikan seluruh data user dengan data baru yang dikirim. Perhatikan bahwa kita harus mengirim semua field (name dan email), bukan hanya field yang berubah.
Hapus User (DELETE)
curl -X DELETE http://localhost:8080/users/1 Response:
{"message": "user deleted"} Ringkasan Endpoint
| Method | Endpoint | Deskripsi |
|---|---|---|
GET | /users | Ambil semua user |
GET | /users/{id} | Ambil user by ID |
POST | /users | Buat user baru |
PUT | /users/{id} | Update user by ID |
DELETE | /users/{id} | Hapus user by ID |
Catatan Penting
- Thread-safe: Repository menggunakan
sync.RWMutexagar aman untuk concurrent request. - In-memory: Data hilang saat server direstart. Untuk produksi, gunakan database seperti PostgreSQL atau MySQL.
- Tidak ada framework: Hanya menggunakan
net/http,encoding/json, dan package standar Go lainnya. - Modular: Struktur folder dipisah per tanggung jawab (model, repository, handler, router) agar mudah dikembangkan.
Langkah Selanjutnya (Opsional)
- Tambahkan koneksi database dengan
database/sql+ driverpgx(PostgreSQL) - Tambahkan middleware untuk logging dan autentikasi JWT
- Tambahkan validasi input yang lebih ketat
- Buat Dockerfile untuk containerisasi
- Tulis unit test menggunakan package
testing