Kodeloka
  1. Kodeloka
  2. Kelas Golang
  3. Materi

Domain 27 Β· 3 bab

Mini Project Go

265. Mini Project: REST API CRUD Server

Bangun REST API CRUD lengkap tanpa framework eksternal menggunakan net/http, encoding/json, dan in-memory store.

Mini Project: REST API CRUD tanpa Framework

Kita akan membangun REST API sederhana dengan fitur Create, Read, Update, Delete (CRUD) menggunakan hanya standard library Go β€” tanpa Gin, Echo, atau Fiber.

πŸ“‹ Spesifikasi API
  • GET /products β€” Ambil semua produk
  • GET /products/{id} β€” Ambil produk by ID
  • POST /products β€” Buat produk baru
  • PUT /products/{id} β€” Update produk
  • DELETE /products/{id} β€” Hapus produk

Struktur Data

Go β€” Data Model
type Product struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Price     float64   `json:"price"`
    Stock     int       `json:"stock"`
    CreatedAt time.Time `json:"created_at"`
}

// In-memory store (production: ganti dengan database)
var store = struct {
    sync.RWMutex
    products map[int]Product
    nextID   int
}{products: make(map[int]Product), nextID: 1}

Teknik JSON Response Helper

Go
func jsonResponse(w http.ResponseWriter, statusCode int, data any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)
    json.NewEncoder(w).Encode(data)
}

func jsonError(w http.ResponseWriter, statusCode int, message string) {
    jsonResponse(w, statusCode, map[string]string{"error": message})
}

Routing Manual dengan ServeMux

Go 1.22+ β€” Enhanced ServeMux
mux := http.NewServeMux()

// Go 1.22+: mendukung method prefix dan path parameters
mux.HandleFunc("GET /products", listProducts)
mux.HandleFunc("GET /products/{id}", getProduct)
mux.HandleFunc("POST /products", createProduct)
mux.HandleFunc("PUT /products/{id}", updateProduct)
mux.HandleFunc("DELETE /products/{id}", deleteProduct)

Contoh program

package main

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

// ── Data Model ──
type Product struct {
	ID        int       `json:"id"`
	Name      string    `json:"name"`
	Price     float64   `json:"price"`
	Stock     int       `json:"stock"`
	CreatedAt time.Time `json:"created_at"`
}

// ── In-Memory Store ──
var store = struct {
	sync.RWMutex
	products map[int]Product
	nextID   int
}{products: make(map[int]Product), nextID: 1}

// ── Helper Functions ──
func jsonResponse(w http.ResponseWriter, code int, data any) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	json.NewEncoder(w).Encode(data)
}

func getIDFromPath(path string) (int, error) {
	parts := strings.Split(strings.Trim(path, "/"), "/")
	if len(parts) < 2 {
		return 0, fmt.Errorf("no id")
	}
	return strconv.Atoi(parts[len(parts)-1])
}

// ── Handlers ──
func listProducts(w http.ResponseWriter, r *http.Request) {
	store.RLock()
	defer store.RUnlock()
	products := make([]Product, 0, len(store.products))
	for _, p := range store.products {
		products = append(products, p)
	}
	jsonResponse(w, 200, products)
}

func createProduct(w http.ResponseWriter, r *http.Request) {
	var p Product
	if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
		jsonResponse(w, 400, map[string]string{"error": "invalid JSON"})
		return
	}
	store.Lock()
	defer store.Unlock()
	p.ID = store.nextID
	p.CreatedAt = time.Now()
	store.products[p.ID] = p
	store.nextID++
	jsonResponse(w, 201, p)
}

func main() {
	// Seed data awal
	store.products[1] = Product{ID: 1, Name: "Laptop Go", Price: 15000000, Stock: 10, CreatedAt: time.Now()}
	store.products[2] = Product{ID: 2, Name: "Go Book", Price: 150000, Stock: 50, CreatedAt: time.Now()}
	store.nextID = 3

	mux := http.NewServeMux()
	mux.HandleFunc("/products", func(w http.ResponseWriter, r *http.Request) {
		switch r.Method {
		case "GET":
			listProducts(w, r)
		case "POST":
			createProduct(w, r)
		default:
			jsonResponse(w, 405, map[string]string{"error": "method not allowed"})
		}
	})

	fmt.Println("REST API Server berjalan di :8080")
	fmt.Println("GET  /products         β†’ list semua produk")
	fmt.Println("POST /products         β†’ buat produk baru")
	http.ListenAndServe(":8080", mux)
}

Praktik dan kuis bab ini di kelas interaktif β†’

266. Mini Project: CLI Tool dengan Flag & Args

Buat CLI tool interaktif menggunakan package flag, os.Args, bufio, dan os β€” lengkap dengan subcommand dan help text.

Mini Project: CLI Tool Go

Go sangat cocok untuk membuat Command Line Interface (CLI) tool karena menghasilkan single binary tanpa dependency eksternal. Kita akan membuat tool manajemen catatan sederhana.

πŸ“‹ Fitur CLI Tool
  • mynote add "Isi catatan" β€” Tambah catatan baru
  • mynote list β€” Tampilkan semua catatan
  • mynote delete 3 β€” Hapus catatan ke-3
  • mynote --help β€” Tampilkan bantuan
  • mynote --version β€” Tampilkan versi

Package flag β€” Parsing Arguments

Go β€” flag package
import "flag"

// Definisikan flags
version := flag.Bool("version", false, "Tampilkan versi aplikasi")
output  := flag.String("output", "text", "Format output (text/json)")
limit   := flag.Int("limit", 10, "Jumlah item yang ditampilkan")

// Parse semua flags
flag.Parse()

// Argumen yang tersisa setelah flags
args := flag.Args() // []string
subcommand := args[0] // "add", "list", "delete"

Menyimpan Data ke File JSON

Go β€” Persist data ke file
import (
    "encoding/json"
    "os"
)

type Note struct {
    ID        int       `json:"id"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
}

func loadNotes(filename string) []Note {
    data, err := os.ReadFile(filename)
    if err != nil {
        return []Note{} // File belum ada
    }
    var notes []Note
    json.Unmarshal(data, Β¬es)
    return notes
}

func saveNotes(filename string, notes []Note) error {
    data, _ := json.MarshalIndent(notes, "", "  ")
    return os.WriteFile(filename, data, 0644)
}

Contoh program

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"strconv"
	"time"
)

const (
	Version   = "1.0.0"
	DataFile  = "notes.json"
)

type Note struct {
	ID        int       `json:"id"`
	Content   string    `json:"content"`
	CreatedAt time.Time `json:"created_at"`
}

func loadNotes() []Note {
	data, err := os.ReadFile(DataFile)
	if err != nil {
		return []Note{}
	}
	var notes []Note
	json.Unmarshal(data, &notes)
	return notes
}

func saveNotes(notes []Note) {
	data, _ := json.MarshalIndent(notes, "", "  ")
	os.WriteFile(DataFile, data, 0644)
}

func addNote(content string) {
	notes := loadNotes()
	nextID := 1
	if len(notes) > 0 {
		nextID = notes[len(notes)-1].ID + 1
	}
	notes = append(notes, Note{
		ID: nextID, Content: content, CreatedAt: time.Now(),
	})
	saveNotes(notes)
	fmt.Printf("βœ… Catatan #%d ditambahkan!\n", nextID)
}

func listNotes() {
	notes := loadNotes()
	if len(notes) == 0 {
		fmt.Println("πŸ“­ Belum ada catatan.")
		return
	}
	fmt.Printf("πŸ“‹ Total %d catatan:\n\n", len(notes))
	for _, n := range notes {
		fmt.Printf("[%d] %s\n    πŸ“… %s\n\n",
			n.ID, n.Content, n.CreatedAt.Format("2006-01-02 15:04"))
	}
}

func deleteNote(id int) {
	notes := loadNotes()
	newNotes := make([]Note, 0)
	deleted := false
	for _, n := range notes {
		if n.ID != id {
			newNotes = append(newNotes, n)
		} else {
			deleted = true
		}
	}
	if deleted {
		saveNotes(newNotes)
		fmt.Printf("πŸ—‘οΈ Catatan #%d dihapus!\n", id)
	} else {
		fmt.Printf("❌ Catatan #%d tidak ditemukan.\n", id)
	}
}

func main() {
	versionFlag := flag.Bool("version", false, "Tampilkan versi")
	flag.Usage = func() {
		fmt.Fprintf(os.Stderr, `MyNote CLI v%s - Manajemen Catatan Go

Usage: mynote <command> [arguments]

Commands:
  add <catatan>   Tambah catatan baru
  list            Tampilkan semua catatan
  delete <id>     Hapus catatan berdasarkan ID

Flags:
`, Version)
		flag.PrintDefaults()
	}
	flag.Parse()

	if *versionFlag {
		fmt.Println("MyNote CLI v" + Version)
		return
	}

	args := flag.Args()
	if len(args) == 0 {
		flag.Usage()
		return
	}

	switch args[0] {
	case "add":
		if len(args) < 2 {
			fmt.Println("❌ Gunakan: mynote add <isi catatan>")
			return
		}
		addNote(args[1])
	case "list":
		listNotes()
	case "delete":
		if len(args) < 2 {
			fmt.Println("❌ Gunakan: mynote delete <id>")
			return
		}
		id, err := strconv.Atoi(args[1])
		if err != nil {
			fmt.Println("❌ ID harus berupa angka")
			return
		}
		deleteNote(id)
	default:
		fmt.Printf("❌ Perintah tidak dikenal: %s\n", args[0])
		flag.Usage()
	}
}

Praktik dan kuis bab ini di kelas interaktif β†’

267. Mini Project: Simple File-based Task Manager

Bangun task manager sederhana berbasis JSON file dengan operasi CRUD, status tracking, dan filter β€” menggunakan konsep OOP Go dengan struct dan methods.

Mini Project: Task Manager Go

Project ini menggabungkan berbagai konsep Go yang sudah dipelajari: struct, methods, interfaces, JSON serialization, error handling, dan file I/O.

🎯 Konsep yang Dipraktikkan
  • βœ… Struct dengan methods (OOP style Go)
  • βœ… JSON encode/decode untuk persistensi data
  • βœ… Error handling idiomatik Go
  • βœ… Slice filtering dan sorting
  • βœ… Interface untuk extensibility
  • βœ… Goroutine untuk background auto-save

Arsitektur Task Manager

Go β€” Struktur Data
type Priority string
const (
    Low    Priority = "low"
    Medium Priority = "medium"
    High   Priority = "high"
)

type Task struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description"`
    Priority    Priority  `json:"priority"`
    Done        bool      `json:"done"`
    DueDate     time.Time `json:"due_date"`
    CreatedAt   time.Time `json:"created_at"`
}

// TaskManager sebagai type dengan methods
type TaskManager struct {
    tasks    []Task
    filePath string
    mu       sync.Mutex
}

// Interface untuk filtering
type TaskFilter interface {
    Filter(tasks []Task) []Task
}

// Filter implementations
type DoneFilter struct{}
type PriorityFilter struct { P Priority }

func (f DoneFilter) Filter(tasks []Task) []Task { ... }
func (f PriorityFilter) Filter(tasks []Task) []Task { ... }

Contoh program

package main

import (
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"time"
)

type Priority string

const (
	Low    Priority = "low"
	Medium Priority = "medium"
	High   Priority = "high"
)

type Task struct {
	ID        int       `json:"id"`
	Title     string    `json:"title"`
	Priority  Priority  `json:"priority"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"created_at"`
}

type TaskManager struct {
	Tasks    []Task `json:"tasks"`
	NextID   int    `json:"next_id"`
	filePath string
}

func NewTaskManager(filePath string) *TaskManager {
	tm := &TaskManager{filePath: filePath, NextID: 1}
	tm.load()
	return tm
}

func (tm *TaskManager) load() {
	data, err := os.ReadFile(tm.filePath)
	if err != nil {
		return
	}
	json.Unmarshal(data, tm)
}

func (tm *TaskManager) save() error {
	data, err := json.MarshalIndent(tm, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(tm.filePath, data, 0644)
}

func (tm *TaskManager) Add(title string, priority Priority) Task {
	task := Task{
		ID: tm.NextID, Title: title,
		Priority: priority, CreatedAt: time.Now(),
	}
	tm.Tasks = append(tm.Tasks, task)
	tm.NextID++
	tm.save()
	return task
}

func (tm *TaskManager) Complete(id int) bool {
	for i, t := range tm.Tasks {
		if t.ID == id {
			tm.Tasks[i].Done = true
			tm.save()
			return true
		}
	}
	return false
}

func (tm *TaskManager) FilterByPriority(p Priority) []Task {
	var result []Task
	for _, t := range tm.Tasks {
		if t.Priority == p {
			result = append(result, t)
		}
	}
	return result
}

func (tm *TaskManager) SortedByPriority() []Task {
	copy := make([]Task, len(tm.Tasks))
	for i, t := range tm.Tasks {
		copy[i] = t
	}
	priOrder := map[Priority]int{High: 0, Medium: 1, Low: 2}
	sort.Slice(copy, func(i, j int) bool {
		return priOrder[copy[i].Priority] < priOrder[copy[j].Priority]
	})
	return copy
}

func (tm *TaskManager) Stats() (total, done, pending int) {
	total = len(tm.Tasks)
	for _, t := range tm.Tasks {
		if t.Done {
			done++
		} else {
			pending++
		}
	}
	return
}

func printTask(t Task) {
	status := "⬜"
	if t.Done {
		status = "βœ…"
	}
	priEmoji := map[Priority]string{High: "πŸ”΄", Medium: "🟑", Low: "🟒"}
	fmt.Printf("%s [%d] %s %s\n", status, t.ID, priEmoji[t.Priority], t.Title)
}

func main() {
	fmt.Println("=== Go Task Manager ===\n")

	tm := NewTaskManager("tasks.json")

	// Demo: Tambah tasks
	t1 := tm.Add("Setup CI/CD Pipeline", High)
	t2 := tm.Add("Tulis unit tests", Medium)
	t3 := tm.Add("Update dokumentasi README", Low)
	tm.Add("Deploy ke production server", High)
	tm.Add("Review pull requests", Medium)

	// Complete salah satu
	tm.Complete(t1.ID)
	tm.Complete(t2.ID)

	_ = t3 // suppress unused

	// Tampilkan sorted
	fmt.Println("πŸ“‹ Semua Task (sorted by priority):")
	for _, t := range tm.SortedByPriority() {
		printTask(t)
	}

	// Stats
	total, done, pending := tm.Stats()
	fmt.Printf("\nπŸ“Š Stats: %d total | %d selesai | %d pending\n", total, done, pending)

	// Filter high priority yang belum selesai
	fmt.Println("\nπŸ”΄ High Priority (pending):")
	for _, t := range tm.FilterByPriority(High) {
		if !t.Done {
			printTask(t)
		}
	}
}

Praktik dan kuis bab ini di kelas interaktif β†’