Implementasi dan Penguatan

Program bebas race, terukur dengan benchmark, dan teruji.

Daftar isi

Struktur tiga paket sudah berdiri, tetapi layanan belum menerima satu pun permintaan dan belum ada bukti bahwa kode itu benar, bebas race, dan cukup cepat. Topik ini memasang lapisan HTTP proyek, menulis pengujian untuk setiap paket, menjalankan detektor race, mengukur worker pool dengan benchmark, dan menutup pengerjaan dengan pemeriksaan statis. Perangkat pengujian, httptest, dan pola benchmark dipakai sebagaimana telah diperkenalkan pada modul Pengujian.

Lapisan HTTP dan penyambungan server#

Dua berkas baru masuk ke proyek, yaitu handler.go yang menyediakan lapisan HTTP dan handler_test.go yang mengujinya, ditambah berkas pengujian pada kedua paket internal. Pohon proyek lengkapnya sebagaimana berikut.

text
statistik-teks/
├── go.mod
├── main.go
├── handler.go
├── handler_test.go
└── internal/
    ├── analisis/
    │   ├── analisis.go
    │   └── analisis_test.go
    └── teks/
        ├── teks.go
        └── teks_test.go

Kedua berkas pustaka ditampilkan kembali tanpa perubahan satu baris pun, sehingga seluruh perubahan pada topik ini terkonsentrasi pada lapisan HTTP dan berkas pengujian. Berkas pustaka yang dimaksud adalah internal/teks/teks.go untuk analisis murni:

go internal/teks/teks.go
// tidak-dijalankan: bagian dari proyek multi-paket

// Package teks menyediakan analisis statistik satu potongan teks.
package teks

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

// Statistik menyimpan hasil analisis satu potongan teks. Tag json pada
// setiap field menyiapkan nama field JSON untuk lapisan HTTP proyek.
type Statistik struct {
	JumlahKata     int    `json:"jumlah_kata"`
	JumlahKarakter int    `json:"jumlah_karakter"`
	KataTerpanjang string `json:"kata_terpanjang"`
	DurasiBaca     string `json:"durasi_baca"`
}

// kataPerMenit adalah asumsi kecepatan baca untuk estimasi durasi.
const kataPerMenit = 200

// Analisis menghitung statistik satu potongan teks.
func Analisis(potongan string) Statistik {
	kata := strings.Fields(potongan)
	terpanjang := ""
	for _, k := range kata {
		if utf8.RuneCountInString(k) > utf8.RuneCountInString(terpanjang) {
			terpanjang = k
		}
	}
	detik := float64(len(kata)) / kataPerMenit * 60
	return Statistik{
		JumlahKata:     len(kata),
		JumlahKarakter: utf8.RuneCountInString(potongan),
		KataTerpanjang: terpanjang,
		DurasiBaca:     fmt.Sprintf("%.1f detik", detik),
	}
}

dan internal/analisis/analisis.go untuk worker pool beserta aliran context:

go internal/analisis/analisis.go
// tidak-dijalankan: bagian dari proyek multi-paket

// Package analisis menganalisis banyak potongan teks secara konkuren
// dengan worker pool berukuran tetap.
package analisis

import (
	"context"
	"sync"

	"contoh/internal/teks"
)

// Hasil menyimpan statistik satu potongan beserta posisinya pada
// masukan, sehingga hasil akhir dapat disusun kembali menurut urutan
// masukan.
type Hasil struct {
	Posisi    int
	Statistik teks.Statistik
}

// Jalankan menganalisis seluruh potongan dengan sejumlah pekerja
// konkuren, lalu mengembalikan hasil yang terurut menurut posisi
// masukan. Context yang dibatalkan mengembalikan error dan menghentikan
// seluruh pekerja.
func Jalankan(ctx context.Context, potongan []string, jumlahPekerja int) ([]teks.Statistik, error) {
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	if jumlahPekerja < 1 {
		jumlahPekerja = 1
	}

	tugas := make(chan int)
	hasil := make(chan Hasil)

	// fan-out: pekerja menarik posisi dari channel tugas yang sama
	var wg sync.WaitGroup
	wg.Add(jumlahPekerja)
	for i := 0; i < jumlahPekerja; i++ {
		go func() {
			defer wg.Done()
			for posisi := range tugas {
				select {
				case hasil <- Hasil{Posisi: posisi, Statistik: teks.Analisis(potongan[posisi])}:
				case <-ctx.Done():
					return
				}
			}
		}()
	}

	// pengirim tugas berhenti lebih awal apabila context dibatalkan
	go func() {
		defer close(tugas)
		for posisi := range potongan {
			select {
			case tugas <- posisi:
			case <-ctx.Done():
				return
			}
		}
	}()

	// fan-in: channel hasil ditutup setelah seluruh pekerja selesai
	go func() {
		wg.Wait()
		close(hasil)
	}()

	keluaran := make([]teks.Statistik, len(potongan))
	for h := range hasil {
		keluaran[h.Posisi] = h.Statistik
	}
	if err := ctx.Err(); err != nil {
		return nil, err
	}
	return keluaran, nil
}

Karena kedua paket ini sudah ditata sejak awal proyek, handler cukup mengimpornya tanpa menyentuh isi, dan pengujian baru ditulis pada berkas _test.go yang menyertai masing-masing paket. Penempatan itu dimungkinkan oleh batas paket yang satu arah, yaitu lapisan HTTP boleh berganti tanpa menyentuh perhitungan dan konkurensi paket analisis.

Berkas handler.go menerjemahkan kontrak POST /statistik menjadi kode. Handler membaca tubuh JSON, menolak permintaan yang tidak sah dengan status yang sudah ditetapkan kontrak, menjalankan analisis.Jalankan dengan pekerja sebanyak empat, lalu menuliskan hasilnya sebagai JSON:

go handler.go
// tidak-dijalankan: bagian dari proyek multi-paket

// handler.go menyediakan lapisan HTTP proyek, yaitu handler untuk
// POST /statistik beserta penyusunan mux-nya.
package main

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

	"contoh/internal/analisis"
	"contoh/internal/teks"
)

// batasPotongan membatasi banyaknya potongan pada satu permintaan.
const batasPotongan = 100

// pekerjaAnalisis adalah jumlah pekerja yang melayani satu permintaan.
const pekerjaAnalisis = 4

// handlerStatistik membaca daftar potongan dari tubuh JSON permintaan,
// menganalisisnya secara konkuren, lalu menuliskan hasilnya sebagai
// JSON. Context permintaan diteruskan agar pembatalan oleh client
// menghentikan pekerjaan.
func handlerStatistik(w http.ResponseWriter, r *http.Request) {
	var permintaan struct {
		Potongan []string `json:"potongan"`
	}
	if err := json.NewDecoder(r.Body).Decode(&permintaan); err != nil {
		http.Error(w, "tubuh permintaan bukan JSON yang sah", http.StatusBadRequest)
		return
	}
	if len(permintaan.Potongan) == 0 {
		http.Error(w, "potongan tidak boleh kosong", http.StatusBadRequest)
		return
	}
	if len(permintaan.Potongan) > batasPotongan {
		http.Error(w, "terlalu banyak potongan", http.StatusRequestEntityTooLarge)
		return
	}

	hasil, err := analisis.Jalankan(r.Context(), permintaan.Potongan, pekerjaAnalisis)
	if err != nil {
		http.Error(w, "permintaan dibatalkan sebelum selesai", http.StatusRequestTimeout)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(struct {
		Hasil []teks.Statistik `json:"hasil"`
	}{Hasil: hasil})
}

// muxBaru menyusun rute layanan, yaitu satu-satunya tempat pola rute
// dideklarasikan sehingga main dan pengujian memakai rute yang sama.
func muxBaru() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /statistik", handlerStatistik)
	return mux
}

Panggilan analisis.Jalankan(r.Context(), ...) menuntut aliran context yang sudah dirancang, yaitu context mengalir sebagai parameter dari pemanggil sampai ke pekerja. Context milik permintaan dibatalkan oleh server ketika koneksi client tertutup, dan pembatalan itu mengalir sampai ke pengirim tugas beserta seluruh pekerja, sehingga layanan tidak menghabiskan tenaga untuk permintaan yang sudah ditinggalkan. Fungsi muxBaru menjadi satu-satunya tempat pola rute dideklarasikan, sehingga fungsi main dan seluruh pengujian memakai rute yang persis sama.

Berkas main.go kini menjadi server utuh yang melayani mux hasil muxBaru pada port 8080 dan mematikan server secara teratur ketika sinyal interupsi diterima:

go main.go
// tidak-dijalankan: server menunggu sinyal dan diuji melalui handler_test.go

// Program statistik-teks menyalakan layanan HTTP pada port 8080 dan
// mematikannya secara teratur ketika sinyal interupsi diterima.
package main

import (
	"context"
	"errors"
	"log"
	"net/http"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	srv := &http.Server{Addr: ":8080", Handler: muxBaru()}

	ctxBerhenti, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer stop()

	go func() {
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			log.Println("error:", err)
		}
	}()

	<-ctxBerhenti.Done()
	ctxMatikan, batal := context.WithTimeout(context.Background(), 5*time.Second)
	defer batal()
	if err := srv.Shutdown(ctxMatikan); err != nil {
		log.Println("error:", err)
	}
}

Komentar pertama berkas itu menegaskan alasan berkas server tidak pernah dijalankan sendirian, yaitu server menunggu sinyal dan hanya berhenti ketika sinyal itu datang, sehingga keluarannya tidak mungkin direproduksi. Kebenaran perilakunya justru dibuktikan lewat pengujian pada bagian berikutnya, dengan server uji yang berumur beberapa milidetik.

Pengujian handler dengan httptest#

Berkas handler_test.go menguji keempat perilaku kontrak, yaitu respons sukses, penolakan tubuh kosong, penolakan metode yang salah, dan jalur jaringan penuh:

go handler_test.go
// tidak-dijalankan: dijalankan melalui go test

package main

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"contoh/internal/teks"
)

func TestHandlerStatistik(t *testing.T) {
	tubuh := `{"potongan": ["satu dua tiga", "empat lima"]}`
	req := httptest.NewRequest(http.MethodPost, "/statistik", strings.NewReader(tubuh))
	rec := httptest.NewRecorder()

	handlerStatistik(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("kode status = %d, seharusnya %d", rec.Code, http.StatusOK)
	}
	var respons struct {
		Hasil []teks.Statistik `json:"hasil"`
	}
	if err := json.Unmarshal(rec.Body.Bytes(), &respons); err != nil {
		t.Fatalf("tubuh respons bukan JSON yang sah: %v", err)
	}
	if len(respons.Hasil) != 2 {
		t.Fatalf("banyak hasil = %d, seharusnya 2", len(respons.Hasil))
	}
	if respons.Hasil[0].JumlahKata != 3 {
		t.Errorf("jumlah kata hasil pertama = %d, seharusnya 3", respons.Hasil[0].JumlahKata)
	}
	if respons.Hasil[1].KataTerpanjang != "empat" {
		t.Errorf("kata terpanjang hasil kedua = %q, seharusnya %q", respons.Hasil[1].KataTerpanjang, "empat")
	}
}

func TestHandlerStatistikPotonganKosong(t *testing.T) {
	req := httptest.NewRequest(http.MethodPost, "/statistik", strings.NewReader(`{"potongan": []}`))
	rec := httptest.NewRecorder()

	handlerStatistik(rec, req)

	if rec.Code != http.StatusBadRequest {
		t.Errorf("kode status = %d, seharusnya %d", rec.Code, http.StatusBadRequest)
	}
}

func TestHandlerRuteMetodeSalah(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/statistik", nil)
	rec := httptest.NewRecorder()

	muxBaru().ServeHTTP(rec, req)

	if rec.Code != http.StatusMethodNotAllowed {
		t.Errorf("kode status = %d, seharusnya %d", rec.Code, http.StatusMethodNotAllowed)
	}
}

func TestHandlerDenganServer(t *testing.T) {
	server := httptest.NewServer(muxBaru())
	defer server.Close()

	resp, err := http.Post(server.URL+"/statistik", "application/json", strings.NewReader(`{"potongan": ["satu dua tiga"]}`))
	if err != nil {
		t.Fatalf("permintaan ke server uji gagal: %v", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("kode status = %d, seharusnya %d", resp.StatusCode, http.StatusOK)
	}
	var respons struct {
		Hasil []teks.Statistik `json:"hasil"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&respons); err != nil {
		t.Fatalf("tubuh respons bukan JSON yang sah: %v", err)
	}
	if len(respons.Hasil) != 1 || respons.Hasil[0].KataTerpanjang != "satu" {
		t.Errorf("hasil server uji tidak sesuai: %+v", respons.Hasil)
	}
}

Tiga pengujian pertama memanggil handler atau mux langsung dengan httptest.NewRequest dan httptest.NewRecorder, sehingga tidak ada port yang dibuka dan tidak ada waktu tunggu yang menumpuk. Pengujian keempat memakai httptest.NewServer yang menyediakan server sungguhan pada loopback, sehingga permintaan melewati client HTTP beserta seluruh jalur jaringannya. Empatnya dijalankan sekaligus dengan pemilihan -run:

bash
# salin seluruh berkas proyek ke folder kerja
rm -rf /tmp/statistik-teks
mkdir -p /tmp/statistik-teks
cp main.go handler.go handler_test.go /tmp/statistik-teks/
cp -r internal /tmp/statistik-teks/
(cd /tmp/statistik-teks && go mod init contoh >/dev/null 2>&1)
# jalankan pengujian handler beserta angka waktunya yang dirapikan
cd /tmp/statistik-teks
go test -v -count=1 -run 'TestHandler' . 2>&1 | sed -E 's/[[:space:]]+/ /g; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; s/\(cached\)/(WAKTU)/'
text Keluaran
=== RUN TestHandlerStatistik
--- PASS: TestHandlerStatistik (WAKTU)
=== RUN TestHandlerStatistikPotonganKosong
--- PASS: TestHandlerStatistikPotonganKosong (WAKTU)
=== RUN TestHandlerRuteMetodeSalah
--- PASS: TestHandlerRuteMetodeSalah (WAKTU)
=== RUN TestHandlerDenganServer
--- PASS: TestHandlerDenganServer (WAKTU)
PASS
ok contoh (WAKTU)

Angka waktu pada keluaran berubah antar eksekusi, sehingga disaring dengan sed menjadi (WAKTU) dan tidak pernah diklaim sebagai nilai tetap. Yang penting dari keluaran itu adalah hasilnya, yaitu empat pengujian handler lolos tanpa satu pun kegagalan. Kontrak yang ditulis pada awal proyek kini ditegakkan oleh go test, bukan lagi oleh kesepakatan lisan.

Pengujian paket internal#

Paket internal mendapat pengujian masing-masing. Paket teks diuji dengan pola table-driven yang mencakup teks biasa, kata yang sama panjang, dan teks kosong:

go internal/teks/teks_test.go
// tidak-dijalankan: dijalankan melalui go test

package teks

import "testing"

func TestAnalisis(t *testing.T) {
	kasus := []struct {
		nama     string
		potongan string
		ingin    Statistik
	}{
		{
			nama:     "teks sederhana",
			potongan: "satu dua tiga",
			ingin:    Statistik{JumlahKata: 3, JumlahKarakter: 13, KataTerpanjang: "satu", DurasiBaca: "0.9 detik"},
		},
		{
			nama:     "kata sama panjang memilih yang pertama",
			potongan: "ini itu",
			ingin:    Statistik{JumlahKata: 2, JumlahKarakter: 7, KataTerpanjang: "ini", DurasiBaca: "0.6 detik"},
		},
		{
			nama:     "teks kosong",
			potongan: "",
			ingin:    Statistik{JumlahKata: 0, JumlahKarakter: 0, KataTerpanjang: "", DurasiBaca: "0.0 detik"},
		},
	}
	for _, k := range kasus {
		t.Run(k.nama, func(t *testing.T) {
			if hasil := Analisis(k.potongan); hasil != k.ingin {
				t.Errorf("Analisis(%q) = %+v, seharusnya %+v", k.potongan, hasil, k.ingin)
			}
		})
	}
}

Paket analisis diuji pada dua sifatnya yang paling berisiko, yaitu ketertiban hasil dan pembatalan. Pengujian ketertiban membandingkan keluaran Jalankan dengan hasil Analisis langsung. Pengujian pembatalan menjalankan Jalankan dengan context yang sudah dibatalkan dan menuntut error. Keduanya deterministik tanpa jeda dan tanpa waktu tunggu:

go internal/analisis/analisis_test.go
// tidak-dijalankan: dijalankan melalui go test

package analisis

import (
	"context"
	"strings"
	"testing"

	"contoh/internal/teks"
)

func TestJalankanTerurut(t *testing.T) {
	potongan := []string{"satu dua tiga", "empat lima"}

	hasil, err := Jalankan(context.Background(), potongan, 3)
	if err != nil {
		t.Fatalf("Jalankan mengembalikan error: %v", err)
	}
	if len(hasil) != 2 {
		t.Fatalf("banyak hasil = %d, seharusnya 2", len(hasil))
	}
	if hasil[0].JumlahKata != 3 || hasil[1].JumlahKata != 2 {
		t.Errorf("hasil tidak terurut menurut posisi masukan: %+v", hasil)
	}
	if hasil[0] != teks.Analisis(potongan[0]) {
		t.Errorf("hasil pertama berbeda dari Analisis langsung: %+v", hasil[0])
	}
}

func TestJalankanBatal(t *testing.T) {
	ctx, batal := context.WithCancel(context.Background())
	batal()

	if _, err := Jalankan(ctx, []string{"satu dua tiga"}, 2); err == nil {
		t.Error("Jalankan seharusnya mengembalikan error pada context yang sudah dibatalkan")
	}
}

func BenchmarkJalankanSatuPekerja(b *testing.B) {
	benchmarkJalankan(b, 1)
}

func BenchmarkJalankanEmpatPekerja(b *testing.B) {
	benchmarkJalankan(b, 4)
}

func benchmarkJalankan(b *testing.B, jumlahPekerja int) {
	satu := strings.Repeat("kata contoh ", 20)
	potongan := make([]string, 32)
	for i := range potongan {
		potongan[i] = satu
	}
	ctx := context.Background()
	b.ResetTimer()
	for i := 0; i < b.N; i++ {
		if _, err := Jalankan(ctx, potongan, jumlahPekerja); err != nil {
			b.Fatal(err)
		}
	}
}

Berkas pengujian itu memuat dua fungsi benchmark sekaligus, dan pengukurannya dibahas pada bagian akhir. Pengujian kedua paket internal dijalankan satu per satu agar urutan keluarannya pasti:

bash
# uji paket teks
go test -count=1 ./internal/teks 2>&1 | sed -E 's/[[:space:]]+/ /g; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; s/\(cached\)/(WAKTU)/'
# uji paket analisis
go test -count=1 ./internal/analisis 2>&1 | sed -E 's/[[:space:]]+/ /g; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; s/\(cached\)/(WAKTU)/'
text Keluaran
ok contoh/internal/teks (WAKTU)
ok contoh/internal/analisis (WAKTU)

Setiap paket kini punya bukti kebenarannya sendiri, dan seluruh proyek dapat diperiksa dengan satu perintah go test ./... kapan pun. Lapisan pengujian ini yang membuat perubahan berikutnya aman dilakukan, karena setiap pelanggaran kontrak akan dilaporkan pada eksekusi berikutnya.

Memastikan program bebas race#

Pengujian yang lolos belum menyatakan program bebas race, karena race bergantung pada waktu dan dapat lolos dari ribuan eksekusi yang sehat. Flag -race mengompilasi program beserta detektor race, yaitu pemeriksa yang memantau setiap akses memori yang tidak tersinkronisasi selama pengujian berjalan. Seluruh paket dijalankan dengan flag itu:

bash
# uji paket main dengan detektor race
go test -race -count=1 . 2>&1 | sed -E 's/[[:space:]]+/ /g; s/0x[0-9a-f]+/0xALAMAT/g; s/goroutine [0-9]+/goroutine N/g; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; s/\(cached\)/(WAKTU)/'
# uji paket analisis dengan detektor race
go test -race -count=1 ./internal/analisis 2>&1 | sed -E 's/[[:space:]]+/ /g; s/0x[0-9a-f]+/0xALAMAT/g; s/goroutine [0-9]+/goroutine N/g; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; s/\(cached\)/(WAKTU)/'
# uji paket teks dengan detektor race
go test -race -count=1 ./internal/teks 2>&1 | sed -E 's/[[:space:]]+/ /g; s/0x[0-9a-f]+/0xALAMAT/g; s/goroutine [0-9]+/goroutine N/g; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; s/\(cached\)/(WAKTU)/'
text Keluaran
ok contoh (WAKTU)
ok contoh/internal/analisis (WAKTU)
ok contoh/internal/teks (WAKTU)

Ketiga baris ok menyatakan tidak ada laporan race pada seluruh jalur eksekusi yang dilalui pengujian. Apabila laporan race muncul, ia memuat alamat memori berawalan 0x dan nomor ID goroutine yang berubah-ubah antar eksekusi, sehingga keduanya disaring sed menjadi 0xALAMAT dan goroutine N sebelum keluarannya dibandingkan. Proyek ini lolos bukan karena keberuntungan, melainkan karena desainnya menutup sumber race, yaitu seluruh pertukaran data antar goroutine berlangsung lewat channel, sedangkan slice keluaran hanya ditulis oleh goroutine Jalankan itu sendiri.

Mengukur worker pool dengan benchmark#

Dua benchmark pada berkas pengujian paket analisis mengukur konfigurasi pekerja yang berbeda pada beban yang sama, yaitu 32 potongan berisi 40 kata. Persiapan data dilakukan sebelum perulangan dan b.ResetTimer() memisahkannya dari waktu yang diukur:

bash
# jalankan kedua benchmark satu kali per fungsi
go test -run '^$' -bench . -benchtime=1x ./internal/analisis 2>&1 | sed -E 's/[[:space:]]+/ /g; s/^(Benchmark[A-Za-z]*)-[0-9]+ [0-9]+ /\1-N ITERASI /; s/[0-9]+(\.[0-9]+)? ns\/op/WAKTU ns\/op/; s/\([0-9]+\.[0-9]+s\)/(WAKTU)/; s/[0-9]+\.[0-9]+s/(WAKTU)/; /^cpu:/d'
text Keluaran
goos: linux
goarch: amd64
pkg: contoh/internal/analisis
BenchmarkJalankanSatuPekerja-N ITERASI WAKTU ns/op
BenchmarkJalankanEmpatPekerja-N ITERASI WAKTU ns/op
PASS
ok contoh/internal/analisis (WAKTU)

Angka ns/op sebenarnya berubah antar mesin dan antar beban sistem, sehingga keluarannya disaring menjadi WAKTU ns/op. Pada pengukuran nyata di mesin ini, empat pekerja justru sedikit lebih lambat daripada satu pekerja, karena pekerjaan per potongan sangat singkat sehingga biaya koordinasi channel ikut terukur. Hasil itu bukan kegagalan desain, melainkan data untuk pengambilan keputusan. Pekerja konkuren hanya terbayar apabila pekerjaan per unit cukup berat, sedangkan benchmark tetap berguna untuk memantau pergeseran angka ketika kontrak analisis bertambah berat.

Pemeriksaan statis penutup#

Kriteria selesai pada kontrak proyek ditutup dengan dua perintah statis, yaitu gofmt yang memeriksa format dan go vet yang memeriksa pola kode yang mencurigakan:

bash
# pastikan tidak ada berkas yang menyimpang dari format gofmt
test -z "$(gofmt -l .)" && echo "gofmt: tidak ada berkas yang perlu diperbaiki"
# periksa seluruh paket dengan go vet
go vet ./... && echo "go vet: tidak menemukan masalah"
text Keluaran
gofmt: tidak ada berkas yang perlu diperbaiki
go vet: tidak menemukan masalah

Dengan dua baris itu, seluruh butir kriteria selesai terpenuhi, yaitu pengujian lolos pada tiga paket, detektor race bersih, benchmark tercatat, dan pemeriksaan statis senyap. Proyek statistik teks kini berupa layanan HTTP multi-paket yang kontraknya ditegakkan pengujian, konkurensinya dijaga channel, dan pengerjaannya ditutup dengan bukti yang dapat diulang.

Kesalahan umum#

  • Menguji handler hanya melalui server sungguhan. Server yang disalakan untuk setiap pengujian memakai port, menambah waktu tunggu, dan menyisakan keadaan yang harus dibersihkan. Perbaikannya, uji isi respons dengan httptest.NewRecorder, dan sisakan httptest.NewServer untuk jalur jaringan yang benar-benar membutuhkannya.
  • Membiarkan waktu persiapan terukur sebagai waktu benchmark. Data uji yang dibentuk di dalam perulangan benchmark ikut terhitung menjadi ns/op padahal bukan pekerjaan yang ingin diukur. Perbaikannya, pindahkan persiapan ke luar perulangan, lalu panggil b.ResetTimer().
  • Menjalankan pekerjaan yang lama tanpa context permintaan. Pekerjaan yang mengabaikan r.Context() tetap berjalan penuh padahal client sudah memutus koneksi, dan server menanggung beban yang tidak dibutuhkan siapa pun. Perbaikannya, teruskan r.Context() kepada seluruh pekerjaan yang berumur panjang, sebagaimana Jalankan menerimanya.

Ringkasan#

  • Handler menerjemahkan kontrak POST /statistik menjadi validasi, pemanggilan Jalankan dengan r.Context(), dan respons JSON, sedangkan muxBaru menjadi satu-satunya tempat rute dideklarasikan.
  • Handler diuji dengan httptest.NewRecorder untuk isi respons dan httptest.NewServer untuk jalur jaringan penuh.
  • Setiap paket memiliki pengujian deterministik, termasuk pembatalan context yang diuji tanpa waktu tunggu.
  • Detektor race, benchmark dengan angka ternormalisasi, go vet, dan gofmt menutup kriteria selesai yang ditetapkan sejak awal proyek.

Referensi#