09Testing, Table-Driven Tests, Coverage, dan Race Detector
10Proyek Akhir: CLI Task Persisten Berkualitas Produksi
Chapter 1 of 10
Menyiapkan Go, Toolchain, dan Module
Tujuan Pembelajaran
Setelah bab ini, Anda mampu:
memasang dan memverifikasi Go;
mengenali peran perintah utama toolchain;
membuat module dan program pertama;
menjalankan, menguji, memformat, dan membangun program;
memahami hubungan module, package, dan file sumber.
1. Memasang dan Memverifikasi Go
Unduh distribusi resmi yang sesuai sistem operasi, ikuti petunjuk pemasangan, lalu buka terminal baru. Verifikasi instalasi:
Code
go version
go env GOROOT GOPATH GOMOD
GOROOT menunjuk instalasi Go. GOPATH adalah workspace global untuk cache module dan alat yang dipasang; kode aplikasi modern tidak perlu ditempatkan di dalamnya. GOMOD menunjukkan file go.mod aktif atau /dev/null ketika terminal berada di luar module.
Pastikan direktori biner Go tersedia pada PATH. Jangan mengubah GOROOT secara manual kecuali Anda memang mengelola instalasi khusus.
2. Toolchain Inti
Perintah
Kegunaan
go run .
Mengompilasi sementara dan menjalankan package utama
go build .
Memeriksa kompilasi dan menghasilkan biner bila package adalah main
go test ./...
Menjalankan pengujian seluruh package di module
Chapter 2 of 10
Variabel, Konstanta, Tipe, String, dan Rune
Tujuan Pembelajaran
Setelah bab ini, Anda mampu:
mendeklarasikan variabel dan konstanta secara idiomatis;
memahami tipe statis, nilai nol, konversi, dan inferensi;
memilih tipe numerik yang tepat;
mengolah teks UTF-8 dengan string, byte, dan rune.
1. Variabel dan Nilai Nol
Go bertipe statis: tipe setiap nilai diketahui saat kompilasi. Deklarasi var dapat menyertakan tipe, nilai awal, atau keduanya.
Code
package main
{
jumlah
aktif
nama =
usia :=
fmt.Println(jumlah, aktif, nama, usia)
}
Chapter 3 of 10
Alur Kontrol di Go
Tujuan Pembelajaran
Setelah bab ini, Anda mampu:
membuat percabangan dengan if dan switch;
menulis berbagai bentuk perulangan for;
mengiterasi koleksi dengan range;
memakai break dan continue secara terukur;
menghindari jebakan scope dan kondisi batas.
1. if, else if, dan Scope
Kondisi if harus bernilai bool; Go tidak menganggap 0, string kosong, atau nil sebagai false secara implisit. Tanda kurung tidak diperlukan, sedangkan kurung kurawal wajib.
Code
Chapter 4 of 10
Fungsi, Pointer, Error, dan Defer
Tujuan Pembelajaran
Setelah bab ini, Anda mampu:
mendefinisikan fungsi dengan parameter dan beberapa hasil;
memahami kapan pointer diperlukan;
membuat, membungkus, dan memeriksa error;
memakai defer untuk cleanup yang dapat diandalkan.
1. Fungsi dan Nilai Kembalian
Fungsi Go menyatakan tipe setiap parameter dan hasil. Parameter bertipe sama dapat diringkas.
Code
Chapter 5 of 10
Array, Slice, dan Map
Tujuan Pembelajaran
Setelah bab ini, Anda mampu:
membedakan array, slice, dan map;
mengelola panjang dan kapasitas slice;
memahami backing array, append, dan copy;
membedakan koleksi nil dan kosong;
memakai map dengan pola lookup comma-ok.
1. Array: Ukuran Bagian dari Tipe
Array menyimpan elemen berjumlah tetap. Panjangnya menjadi bagian dari tipe; [3]int berbeda dari [4]int.
Code
Chapter 6 of 10
Struct, Method, Interface, dan Komposisi
Tujuan Pembelajaran
Setelah menyelesaikan bab ini, Anda mampu:
memodelkan data dengan struct;
menulis method dengan receiver nilai dan pointer;
mendefinisikan interface kecil berdasarkan perilaku;
memakai komposisi dan embedding tanpa pewarisan;
memilih tipe konkret atau interface secara tepat.
1. Struct: Mengelompokkan Data yang Berkaitan
struct adalah tipe bentukan yang menyatukan beberapa field. Gunakan nama field berawalan huruf besar jika field perlu diakses dari package lain.
Code
Chapter 7 of 10
Package, Struktur Proyek, dan Dependensi
Tujuan Pembelajaran
Setelah menyelesaikan bab ini, Anda mampu:
membagi program menjadi package yang kohesif;
membuat dan merawat module Go;
memilih struktur proyek yang sederhana dan idiomatis;
mengelola dependensi dengan go mod;
memahami aturan ekspor, import, dan package internal.
1. Package dan Module
Package adalah unit organisasi, kompilasi, dan akses kode. Module adalah kumpulan package berversi yang memiliki berkas go.mod.
Buat proyek baru:
Code
mkdir taskcli
cd taskcli
go mod init example.com/taskcli
Nama module biasanya URL repositori tempat kode akan diterbitkan. Untuk eksperimen lokal, nama seperti example.com/taskcli sah selama import di proyek konsisten.
Setiap direktori normal berisi satu package. Nama package singkat, huruf kecil, dan tidak perlu mengulang konteks: gunakan task, bukan taskutils atau task_package.
2. Ekspor dan Import
Identifier berawalan huruf besar diekspor dari package; identifier berawalan huruf kecil hanya dapat digunakan di package yang sama.
Code
Chapter 8 of 10
File I/O, JSON, dan Persistensi CLI
Tujuan Pembelajaran
Setelah menyelesaikan bab ini, Anda mampu:
membaca dan menulis file dengan pustaka standar;
mengubah struct menjadi JSON dan sebaliknya;
membedakan file tidak ada dari kegagalan I/O lain;
menyimpan data CLI secara atomik;
menangani path, permission, dan error dengan aman.
1. Membaca dan Menulis File
Untuk file kecil yang harus dimuat seluruhnya, gunakan os.ReadFile dan os.WriteFile.
Code
Chapter 9 of 10
Testing, Table-Driven Tests, Coverage, dan Race Detector
Tujuan Pembelajaran
Setelah menyelesaikan bab ini, Anda mampu:
menulis unit test dengan package testing;
menyusun table-driven test dan subtest;
menguji file tanpa mencemari lingkungan pengguna;
membaca coverage secara kritis;
mendeteksi data race dengan race detector.
1. Dasar Pengujian Go
File pengujian berakhiran _test.go. Fungsi test bernama TestXxx dan menerima *testing.T.
Code
Chapter 10 of 10
Proyek Akhir: CLI Task Persisten Berkualitas Produksi
Tujuan Pembelajaran
Setelah menyelesaikan proyek ini, Anda mampu:
menyusun CLI menjadi domain, penyimpanan, dan entry point;
memvalidasi input serta mempertahankan invariant data;
menyimpan JSON dengan penggantian file atomik;
menghasilkan pesan error dan exit code yang berguna;
menguji perilaku penting dengan table-driven tests;
memverifikasi format, analisis statis, coverage, dan data race.
Data disimpan dalam JSON. Flag global -file dapat menentukan lokasi file; jika tidak diberikan, aplikasi menggunakan direktori konfigurasi pengguna.
Aturan domain:
judul dipangkas dan tidak boleh kosong;
judul maksimum 200 byte pada versi ini;
ID berupa bilangan bulat positif, unik, dan tidak dipakai ulang;
done dan delete gagal jika ID tidak ditemukan;
kegagalan membaca atau decode tidak boleh ditafsirkan sebagai data kosong;
perubahan baru dianggap berhasil setelah penyimpanan berhasil.
2. Arsitektur
Memuat diagram…
Sampai jumpa di pelajaran berikutnya
Terima kasih telah belajar.
Anda telah menyelesaikan 10 bab dalam Golang Dasar: Dari Nol hingga CLI Production-Ready. Gunakan fondasi ini untuk berlatih, bereksperimen, dan membagikan kembali apa yang Anda pelajari.
Golang Dasar: Dari Nol hingga CLI Production-Ready · Ebook preview
go fmt ./...
Memformat kode dengan gaya standar Go
go vet ./...
Mendeteksi pola kode yang mencurigakan
go mod tidy
Menyelaraskan dependensi di go.mod dan go.sum
go doc fmt.Println
Membaca dokumentasi simbol
Alur kerja sederhananya:
Memuat diagram…
3. Membuat Module Pertama
Module adalah unit versi dan distribusi yang berisi satu atau lebih package. Buat proyek di direktori kosong:
Code
mkdir halo-go
cd halo-go
go mod init example.com/halo-go
Perintah terakhir membuat go.mod:
Code
module example.com/halo-go
go 1.23
Direktif go mengikuti versi bahasa yang dipilih toolchain Anda dan dapat berbeda dari contoh. Untuk proyek yang diterbitkan, path module biasanya sama dengan lokasi repositori. Untuk latihan lokal, path contoh tetap valid.
Buat main.go:
Code
package main
import"fmt"funcmain() {
fmt.Println("Halo, Go!")
}
Jalankan dan bangun:
Code
go fmt ./...
go run .
go build .
package main dan fungsi main menandai program yang dapat dieksekusi. import "fmt" memakai package pustaka standar. Nama package berlaku per direktori: file .go biasa dalam satu direktori harus memakai nama package yang sama.
Huruf awal kapital mengekspor Pesan agar dapat dipakai package lain. Ubah main.go:
Code
package main
import (
"fmt""example.com/halo-go/salam"
)
funcmain() {
fmt.Println(salam.Pesan("Ayu"))
}
Import menggunakan path module ditambah subdirektori package, bukan path filesystem absolut.
5. Dependensi dan Reproduksibilitas
Ketika kode mengimpor module eksternal, perintah Go dapat mengunduh versinya dan mencatatnya di go.mod; checksum konten tersimpan di go.sum. Jalankan:
Code
go mod tidy
go test ./...
Commit go.mod dan go.sum untuk aplikasi. Jangan mengedit go.sum manual. Cache dependensi bukan bagian proyek dan tidak perlu disalin.
Kesalahan Umum
Menjalankan satu file dengan go run main.go pada proyek multifile. Gunakan go run . agar seluruh package dipertimbangkan.
Menaruh beberapa package biasa dalam satu direktori. Pisahkan package ke direktori berbeda.
Menganggap GOPATH sebagai lokasi wajib proyek. Module modern dapat berada di direktori mana pun.
Path import tidak cocok dengan module di go.mod. Gunakan path module sebagai awalan.
Tidak memformat kode. Jalankan go fmt ./...; format standar adalah bagian dari budaya Go.
Mengabaikan hasil go test atau go vet. Keduanya menangkap masalah sebelum biner didistribusikan.
Latihan
Buat module example.com/profil yang mencetak nama dan pekerjaan Anda.
Tambahkan package teks dengan fungsi publik Judul(s string) string, lalu panggil dari main.
Jalankan go fmt ./..., go test ./..., go vet ./..., dan go build .. Catat fungsi setiap perintah.
Ubah nama package pada salah satu file menjadi nama berbeda, amati galat kompilasi, lalu perbaiki.
Gunakan go doc strings.ToUpper untuk membaca kontrak fungsi tanpa peramban.
Rangkuman
Go menyediakan toolchain terpadu untuk format, kompilasi, pengujian, dokumentasi, dan dependensi. go.mod menetapkan identitas module; direktori membentuk package; dan package main menghasilkan program. Siklus aman untuk proyek pemula adalah format, test, vet, lalu build.
import
"fmt"
funcmain()
var
int
var
bool
var
"Nadia"
21
Variabel tanpa initializer memperoleh zero value: 0 untuk angka, false untuk boolean, "" untuk string, dan nil untuk pointer, slice, map, channel, fungsi, serta interface.
:= adalah deklarasi singkat dan hanya berlaku di dalam fungsi. Gunakan var untuk variabel tingkat package atau ketika tipe/nilai nol perlu ditonjolkan.
Deklarasi singkat multivariabel boleh mendeklarasikan sebagian nama baru:
Code
x, y := 10, 20
x, z := 30, 40
fmt.Println(x, y, z)
Baris kedua sah karena z baru; x hanya diberi nilai baru.
2. Konstanta dan Tipe Dasar
Konstanta ditentukan saat kompilasi dan tidak dapat diubah. Konstanta dapat tidak bertipe sampai konteks menuntut tipe tertentu.
Code
package main
import"fmt"const Pi = 3.141592653589793const MaksPercobaan int = 3funcmain() {
var radius float64 = 2
luas := Pi * radius * radius
fmt.Println(luas, MaksPercobaan)
}
Tipe dasar penting:
bool;
string;
integer bertanda int, int8, int16, int32, int64;
integer tanpa tanda uint, uint8, uint16, uint32, uint64;
pecahan float32, float64;
kompleks complex64, complex128;
alias byte untuk uint8 dan rune untuk int32.
Gunakan int untuk hitungan umum dan indeks. Gunakan ukuran eksplisit untuk format data/protokol. Pilih float64 sebagai default pecahan, tetapi jangan memakainya untuk uang jika pembulatan desimal harus eksak; simpan satuan terkecil sebagai integer.
3. Konversi Eksplisit
Go tidak melakukan konversi numerik implisit.
Code
package main
import"fmt"funcmain() {
jumlah := 7
rata := float64(jumlah) / 2
fmt.Println(rata)
}
Konversi dapat kehilangan informasi: int(3.9) menjadi 3, dan konversi ke integer sempit dapat meluap sesuai representasi. Validasi rentang di batas input sebelum mengonversi.
4. String, Byte, Rune, dan UTF-8
string adalah urutan byte yang immutable. Teks Go lazimnya UTF-8, tetapi sebuah string secara teknis dapat memuat byte apa pun. len(s) menghitung byte, bukan karakter pengguna.
Memuat diagram…
Contoh yang memperlihatkan perbedaannya:
Code
package main
import (
"fmt""unicode/utf8"
)
funcmain() {
s := "Go語"
fmt.Println(len(s))
fmt.Println(utf8.RuneCountInString(s))
for indeksByte, r := range s {
fmt.Printf("indeks=%d rune=%q kode=%U\n", indeksByte, r, r)
}
}
語 memerlukan tiga byte UTF-8. range mendekode rune dan memberikan indeks byte awal setiap rune. Rune mewakili code point Unicode, bukan selalu satu karakter yang terlihat; grafem seperti emoji keluarga dapat terdiri dari beberapa rune.
Indeks langsung menghasilkan byte:
Code
package main
import"fmt"funcmain() {
s := "é"
fmt.Printf("%x\n", s[0])
r := []rune(s)
fmt.Printf("%c\n", r[0])
}
Konversi []byte(s) cocok untuk data mentah/UTF-8; []rune(s) cocok ketika operasi memang berbasis code point. Keduanya mengalokasikan salinan yang dapat diubah.
String literal berinterpretasi memakai tanda petik dan escape seperti \n. Raw string memakai backtick dan mempertahankan baris serta backslash:
Code
package main
import"fmt"funcmain() {
path := `C:\data\laporan.txt`
baris := "pertama\nkedua"
fmt.Println(path)
fmt.Println(baris)
}
Kesalahan Umum
Memakai := di tingkat package. Gunakan var atau const.
Mencampur tipe angka tanpa konversi. Konversikan secara eksplisit dan periksa potensi kehilangan data.
Mengambil s[i] dan berharap rune. Hasilnya byte; gunakan range atau []rune bila perlu.
Menganggap rune sama dengan grafem visual. Rune adalah code point, bukan seluruh karakter gabungan.
Membuat variabel yang tidak digunakan. Go menolak variabel lokal dan import yang tidak dipakai.
Melakukan shadowing tanpa sadar.x := dalam blok dalam dapat membuat variabel baru, bukan memperbarui variabel luar.
Latihan
Deklarasikan profil pengguna memakai string, int, dan bool; cetak zero value sebelum pemberian nilai.
Hitung luas lingkaran dengan konstanta tak bertipe dan float64.
Untuk string "Indonesia 🇮🇩", tampilkan jumlah byte, jumlah rune, indeks byte, dan setiap rune.
Buat program yang menerima jumlah detik sebagai int dan menghitung jam, menit, serta detik.
Tunjukkan satu konversi numerik yang kehilangan pecahan, lalu jelaskan hasilnya.
Rangkuman
Deklarasi Go menyeimbangkan kejelasan var dengan ringkasnya :=. Konstanta dapat tetap tidak bertipe hingga digunakan, sedangkan konversi antarangka selalu eksplisit. String menyimpan byte; rune merepresentasikan code point Unicode. Pilih operasi byte, rune, atau grafem sesuai kebutuhan, jangan berdasarkan asumsi bahwa semuanya setara dengan “karakter”.
package main
import"fmt"funckategori(nilai int)string {
if nilai >= 85 {
return"A"
} elseif nilai >= 70 {
return"B"
}
return"C"
}
funcmain() {
fmt.Println(kategori(78))
}
Karena cabang pertama sudah return, else sebenarnya dapat dihilangkan untuk mengurangi indentasi.
if dapat memiliki pernyataan pendek yang scopenya terbatas pada seluruh rantai if:
Code
package main
import (
"fmt""strconv"
)
funcmain() {
if n, err := strconv.Atoi("42"); err != nil {
fmt.Println("input tidak valid")
} else {
fmt.Println(n * 2)
}
}
2. switch
switch Go berhenti otomatis setelah satu kasus cocok; tidak memerlukan break. Beberapa nilai dapat berada dalam satu case.
Switch tanpa ekspresi setara dengan switch true dan berguna untuk kondisi berurutan:
Code
funcsuhuLabel(c float64)string {
switch {
case c < 0:
return"beku"case c < 30:
return"sedang"default:
return"panas"
}
}
Urutan kasus penting: kasus pertama yang benar dipilih. fallthrough ada, tetapi jarang dibutuhkan karena mengeksekusi kasus berikutnya tanpa memeriksa kondisinya.
3. Satu Kata Kunci Perulangan: for
Go memakai for untuk seluruh bentuk perulangan.
Code
package main
import"fmt"funcmain() {
for i := 0; i < 3; i++ {
fmt.Println(i)
}
n := 3for n > 0 {
fmt.Println(n)
n--
}
}
Perulangan tanpa kondisi berjalan sampai dihentikan:
Code
for {
// hentikan dengan break, return, atau kondisi eksternalbreak
}
Aliran keputusan loop:
Memuat diagram…
4. Iterasi dengan range
range menghasilkan pasangan indeks/nilai untuk slice dan array, serta key/value untuk map.
Code
package main
import"fmt"funcmain() {
nilai := []int{80, 90, 75}
total := 0for i, n := range nilai {
fmt.Printf("nilai[%d]=%d\n", i, n)
total += n
}
fmt.Println("total:", total)
}
Gunakan blank identifier untuk hasil yang tidak dibutuhkan:
Code
for _, n := range nilai {
fmt.Println(n)
}
Urutan iterasi map sengaja tidak ditentukan. Jangan mengandalkannya untuk output stabil; kumpulkan dan urutkan key terlebih dahulu jika urutan diperlukan.
Pada string, range menghasilkan indeks byte dan rune yang didekode. Pada slice nilai struct, variabel nilai adalah salinan; ubah elemen melalui indeks bila mutasi diperlukan:
Code
for i := range nilai {
nilai[i] *= 2
}
5. break, continue, dan Label
continue melanjutkan iterasi berikutnya; break keluar dari loop atau switch terdekat.
Code
package main
import"fmt"funcmain() {
for n := 1; n <= 10; n++ {
if n%2 != 0 {
continue
}
if n > 6 {
break
}
fmt.Println(n)
}
}
Label berguna untuk keluar dari loop bersarang, tetapi sering kali fungsi kecil dan return lebih mudah dibaca:
Code
package main
import"fmt"funcmain() {
Luar:
for baris := 0; baris < 3; baris++ {
for kolom := 0; kolom < 3; kolom++ {
if baris == 1 && kolom == 1 {
break Luar
}
fmt.Println(baris, kolom)
}
}
}
Kesalahan Umum
Menulis kondisi non-boolean. Gunakan perbandingan eksplisit, misalnya n != 0.
Menambah break rutin pada setiap case. Switch Go tidak jatuh ke kasus berikutnya secara default.
Loop tak berujung karena lupa memperbarui kondisi. Pastikan setiap jalur memajukan state atau berhenti.
Mengandalkan urutan map. Urutkan key jika hasil harus deterministik.
Memodifikasi salinan dari range. Gunakan indeks untuk mengubah elemen slice.
Off-by-one. Untuk panjang len(data), indeks valid adalah 0 sampai len(data)-1; kondisi lazim adalah i < len(data).
Scope variabel terlalu sempit. Nilai dari short statement if tidak tersedia setelah rantai tersebut.
Latihan
Cetak angka 1–100; ganti kelipatan 3 dengan Fizz, 5 dengan Buzz, dan keduanya dengan FizzBuzz.
Buat fungsi yang mengklasifikasikan usia menggunakan switch tanpa ekspresi.
Hitung nilai minimum, maksimum, dan rata-rata sebuah slice tanpa package tambahan.
Iterasi string beraksara multibyte dan tampilkan indeks byte serta rune.
Buat pencarian pada matriks yang berhenti ketika target ditemukan; implementasikan sekali dengan label dan sekali dengan fungsi yang mengembalikan hasil.
Rangkuman
if menangani keputusan boolean, sedangkan switch menyederhanakan banyak cabang dan berhenti otomatis. Semua perulangan memakai for; range memberi iterasi idiomatis atas koleksi dan string. Gunakan break, continue, dan label hanya ketika membuat alur lebih jelas, serta perhatikan scope, batas indeks, salinan nilai, dan urutan map yang tidak dijamin.
package main
import"fmt"functambah(a, b int)int {
return a + b
}
funcbagi(a, b float64) (float64, bool) {
if b == 0 {
return0, false
}
return a / b, true
}
funcmain() {
fmt.Println(tambah(2, 3))
hasil, ok := bagi(10, 2)
fmt.Println(hasil, ok)
}
Beberapa hasil umum dipakai untuk nilai beserta status atau error. Named result tersedia, tetapi hasil eksplisit biasanya lebih jelas pada fungsi pendek. Fungsi variadik menerima nol atau lebih argumen:
Code
funcjumlah(angka ...int)int {
total := 0for _, n := range angka {
total += n
}
return total
}
Fungsi adalah nilai dan dapat disimpan atau dikirim sebagai argumen:
Code
functerapkan(a, b int, operasi func(int, int)int) int {
return operasi(a, b)
}
2. Pointer dan Semantik Nilai
Argumen selalu diteruskan by value. Jika argumennya pointer, nilai alamat itulah yang disalin, sehingga fungsi dapat mengubah objek tujuan.
Code
package main
import"fmt"funcnaikkan(n *int) {
*n++
}
funcmain() {
nilai := 10
naikkan(&nilai)
fmt.Println(nilai)
}
&nilai mengambil alamat; *n membaca atau menulis nilai yang ditunjuk. Go tidak mendukung aritmetika pointer. Pointer memiliki zero value nil; dereference pointer nil menyebabkan panic.
Gunakan pointer ketika fungsi perlu memutasi nilai pemanggil, ketika penyalinan struct besar memang mahal, atau ketika nil bermakna. Jangan memakai pointer hanya agar kode terlihat efisien. Slice dan map sudah memiliki descriptor/header yang merujuk data internal; fungsi biasanya tidak memerlukan pointer ke slice atau map untuk mengubah elemennya.
3. Error sebagai Nilai
error adalah interface standar:
Code
typeerrorinterface {
Error() string
}
Fungsi yang dapat gagal lazimnya mengembalikan error terakhir. Pemeriksaan dilakukan segera.
Code
package main
import (
"errors""fmt"
)
funcakarSederhana(n float64) (float64, error) {
if n < 0 {
return0, errors.New("nilai tidak boleh negatif")
}
if n == 0 {
return0, nil
}
x := n
for i := 0; i < 10; i++ {
x = (x + n/x) / 2
}
return x, nil
}
funcmain() {
hasil, err := akarSederhana(9)
if err != nil {
fmt.Println("gagal:", err)
return
}
fmt.Println(hasil)
}
Tambahkan konteks tanpa menghilangkan identitas error memakai %w:
Periksa rantai error dengan errors.Is, bukan membandingkan teks. Gunakan errors.As untuk mengambil tipe error tertentu.
Code
package main
import (
"errors""fmt"
)
var ErrTidakDitemukan = errors.New("data tidak ditemukan")
funccari(id int) (string, error) {
if id != 1 {
return"", fmt.Errorf("id %d: %w", id, ErrTidakDitemukan)
}
return"data", nil
}
funcmain() {
_, err := cari(2)
if errors.Is(err, ErrTidakDitemukan) {
fmt.Println("gunakan hasil kosong")
}
}
Alur penanganannya:
Memuat diagram…
4. Cleanup dengan defer
defer menjadwalkan pemanggilan sampai fungsi di sekitarnya selesai, baik melalui return normal maupun panic. Argumen deferred call dievaluasi saat defer dijalankan. Banyak defer dieksekusi LIFO.
Code
package main
import"fmt"funcurutan() {
defer fmt.Println("pertama dijadwalkan, terakhir berjalan")
defer fmt.Println("kedua dijadwalkan, pertama berjalan")
fmt.Println("body")
}
funcmain() {
urutan()
}
Import yang diperlukan adalah errors, fmt, io, dan os. Untuk operasi tulis, error dari Close dapat penting; jangan selalu mengabaikannya. Hindari defer di loop panjang jika resource harus dilepas pada setiap iterasi; pindahkan satu iterasi ke fungsi kecil.
Kesalahan Umum
Mengabaikan error dengan _. Abaikan hanya bila kegagalan benar-benar tidak relevan dan kontraknya dipahami.
Membandingkan err.Error() sebagai teks. Gunakan errors.Is atau errors.As.
Membungkus tanpa %w.%v memutus kemampuan pemeriksaan rantai.
Dereference pointer nil. Validasi nil bila termasuk input yang mungkin.
Menggunakan pointer tanpa kebutuhan. Nilai kecil lebih sederhana dan aman disalin.
Menaruh defer sebelum memastikan resource berhasil dibuka. Jadwalkan cleanup setelah pemeriksaan error.
Salah memahami argumen defer. Nilai argumen ditangkap saat pernyataan defer, bukan saat eksekusi akhir.
Latihan
Buat fungsi variadik yang mengembalikan minimum dan error jika tidak ada argumen.
Buat fungsi tukar(a, b *int) dan uji perubahan pada pemanggil.
Definisikan sentinel error untuk saldo tidak cukup; bungkus dengan konteks dan periksa memakai errors.Is.
Prediksi keluaran tiga defer, lalu jalankan untuk memverifikasi urutan LIFO.
Buat fungsi pembaca file yang selalu menutup file dan menambahkan nama file pada error.
Rangkuman
Fungsi Go mendukung beberapa hasil, nilai fungsi, dan parameter variadik. Semua argumen disalin; pointer memungkinkan mutasi terhadap nilai tujuan, tetapi harus digunakan dengan alasan jelas. Error adalah nilai yang diperiksa, diberi konteks dengan %w, dan dikenali melalui errors.Is/errors.As. defer menempatkan cleanup dekat akuisisi resource dan berjalan dalam urutan LIFO.
package main
import"fmt"funcmain() {
var nol [3]int
prima := [4]int{2, 3, 5, 7}
otomatis := [...]string{"merah", "hijau", "biru"}
fmt.Println(nol, prima, otomatis)
fmt.Println(len(prima), prima[0])
}
Assignment dan parameter array menyalin seluruh array. Array berguna untuk ukuran intrinsik dan tetap, misalnya hash 32 byte. Untuk koleksi umum, slice lebih fleksibel.
2. Slice: Tampilan ke Backing Array
Slice bukan array. Slice adalah descriptor yang mereferensikan segmen backing array, dengan panjang (len) dan kapasitas (cap).
Batas a[low:high] mencakup low dan tidak mencakup high. Kapasitas dihitung dari awal slice sampai akhir backing array yang tersedia.
Memuat diagram…
Buat slice dengan make ketika panjang atau kapasitas diketahui:
Code
package main
import"fmt"funcmain() {
a := make([]int, 3)
b := make([]int, 0, 8)
fmt.Println(a, len(a), cap(a))
fmt.Println(b, len(b), cap(b))
}
make([]int, 3) menghasilkan tiga elemen yang sudah dapat diindeks. make([]int, 0, 8) belum memiliki elemen, sehingga b[0] panic; kapasitas bukan panjang.
3. append, Kapasitas, dan Alias
append mengembalikan slice baru dan hasilnya harus disimpan. Jika kapasitas cukup, backing array lama dapat dipakai; jika tidak, runtime mengalokasikan array baru.
Code
package main
import"fmt"funcmain() {
nilai := make([]int, 0, 2)
nilai = append(nilai, 10, 20)
nilai = append(nilai, 30)
fmt.Println(nilai, len(nilai), cap(nilai))
}
Karena dua slice dapat berbagi backing array, perubahan atau append kadang terlihat melalui slice lain dan kadang tidak setelah realokasi. Jangan bergantung pada detail pertumbuhan kapasitas.
Gunakan full slice expression untuk membatasi kapasitas saat perlu mencegah append menimpa bagian lain:
Code
semua := []int{1, 2, 3, 4}
kiri := semua[:2:2]
kiri = append(kiri, 9)
fmt.Println(semua, kiri)
Kapasitas kiri dibatasi dua, sehingga append memerlukan backing array baru.
4. Menyalin Slice
Assignment slice menyalin header, bukan elemen. Untuk salinan independen, gunakan copy atau append ke slice baru.
copy(dst, src) mengembalikan jumlah elemen yang disalin, yaitu minimum panjang keduanya.
5. Slice Nil dan Kosong
Code
var nilSlice []int
emptySlice := []int{}
madeEmpty := make([]int, 0)
Ketiganya memiliki panjang nol dan aman untuk range serta append. Hanya nilSlice == nil yang benar. Perbedaan dapat tampak pada serialisasi atau API eksternal (null versus []), sehingga ikuti kontrak boundary. Slice tidak dapat dibandingkan dengan slice lain; hanya dapat dibandingkan dengan nil.
6. Map dan comma-ok
Map menyimpan pasangan key-value. Key harus comparable; slice, map, dan function tidak dapat menjadi key.
Lookup key yang tidak ada menghasilkan zero value. Karena zero value mungkin nilai valid, gunakan bentuk nilai, ada := m[key] untuk membedakannya.
Map nil dapat dibaca dan menghasilkan zero value, tetapi penulisan menyebabkan panic:
Code
var m map[string]int
fmt.Println(m["x"])
m = make(map[string]int)
m["x"] = 1
Map adalah reference-like descriptor. Assignment atau pengiriman ke fungsi membuat descriptor baru yang mengacu data map sama. Map tidak aman untuk read/write bersamaan tanpa sinkronisasi. Urutan iterasinya tidak ditentukan.
Pola penghitung:
Code
package main
import"fmt"funcmain() {
frekuensi := make(map[rune]int)
for _, r := range"banana" {
frekuensi[r]++
}
fmt.Println(frekuensi['a'])
}
Zero value int membuat increment langsung bekerja tanpa pemeriksaan keberadaan.
Kesalahan Umum
Menganggap kapasitas sebagai elemen yang dapat diindeks. Indeks harus selalu kurang dari len.
Mengabaikan hasil append. Tulis s = append(s, x).
Mengira assignment slice menyalin data. Gunakan copy untuk independensi.
Tidak menyadari alias backing array. Salin atau batasi kapasitas ketika isolasi diperlukan.
Menulis ke map nil. Inisialisasi dengan literal atau make.
Mengandalkan zero value lookup untuk membuktikan key ada. Gunakan comma-ok.
Mengandalkan urutan iterasi map. Urutkan key untuk keluaran stabil.
Membandingkan dua slice atau map. Keduanya hanya dapat dibandingkan dengan nil.
Latihan
Buat array tujuh suhu harian, lalu hitung rata-ratanya melalui slice.
Buat slice dengan kapasitas awal lima, tambahkan sepuluh elemen, dan cetak len/cap setelah setiap append.
Demonstrasikan dua slice yang berbagi backing array, lalu buat versi independen dengan copy.
Hitung frekuensi setiap kata dalam kalimat memakai map[string]int.
Buat fungsi lookup kontak yang mengembalikan nomor dan bool, termasuk kasus nomor kosong sebagai nilai valid.
Bandingkan perilaku slice nil dan kosong saat len, range, append, dan pemeriksaan == nil.
Rangkuman
Array memiliki ukuran tetap yang menjadi bagian tipe. Slice menyediakan tampilan fleksibel ke backing array melalui pointer, panjang, dan kapasitas; append dapat mempertahankan atau mengganti storage, sedangkan copy membuat elemen independen. Map memberi lookup efisien tetapi tidak menjamin urutan. Pahami nilai nil, aliasing, batas indeks, dan pola comma-ok agar pengelolaan koleksi tetap benar.
package main
import"fmt"type Task struct {
ID int
Title string
Done bool
}
funcmain() {
t := Task{ID: 1, Title: "Belajar struct"}
t.Done = true
fmt.Printf("%d: %s, selesai=%t\n", t.ID, t.Title, t.Done)
}
Utamakan literal bernama seperti Task{ID: 1, Title: "..."}. Bentuk ini tetap jelas ketika urutan field berubah.
2. Method dan Receiver
Method adalah fungsi yang terikat pada tipe. Receiver nilai menerima salinan; receiver pointer dapat mengubah nilai asal.
Code
package main
import (
"errors""fmt""strings"
)
type Task struct {
ID int
Title string
Done bool
}
funcNewTask(id int, title string) (Task, error) {
title = strings.TrimSpace(title)
if id < 1 {
return Task{}, errors.New("ID harus positif")
}
if title == "" {
return Task{}, errors.New("judul tidak boleh kosong")
}
return Task{ID: id, Title: title}, nil
}
func(t *Task) Complete() {
t.Done = true
}
func(t Task) String() string {
status := "[ ]"if t.Done {
status = "[x]"
}
return fmt.Sprintf("%s %d %s", status, t.ID, t.Title)
}
funcmain() {
t, err := NewTask(1, "Belajar method")
if err != nil {
panic(err)
}
t.Complete()
fmt.Println(t)
}
Gunakan receiver pointer ketika method memodifikasi objek atau struct cukup besar. Gunakan receiver nilai untuk operasi baca pada nilai kecil. Jangan mencampur keduanya tanpa alasan yang jelas.
3. Interface: Kontrak Perilaku
Implementasi interface di Go bersifat implisit. Sebuah tipe memenuhi interface jika memiliki seluruh method yang diminta.
Definisikan interface di sisi pemakai, bukan otomatis di sisi implementasi. Terima interface jika fungsi benar-benar membutuhkan variasi perilaku; kembalikan tipe konkret agar API tetap mudah digunakan.
Method Set dan Pointer
Jika method didefinisikan pada *Task, biasanya hanya *Task yang memenuhi interface terkait. Method dengan receiver nilai tersedia bagi Task dan *Task.
Code
var _ fmt.Stringer = Task{}
Pernyataan di atas merupakan pemeriksaan saat kompilasi bahwa Task memenuhi fmt.Stringer.
4. Komposisi dan Embedding
Go tidak memiliki pewarisan kelas. Bangun tipe kompleks dengan menyusun tipe yang lebih kecil.
Memuat diagram…
Embedding mempromosikan field dan method, tetapi nilai embedded tetap merupakan bagian objek, bukan hubungan “adalah”.
Code
package main
import"fmt"type Metadata struct {
CreatedBy string
}
type Task struct {
Metadata
Title string
}
funcmain() {
t := Task{
Metadata: Metadata{CreatedBy: "Ayu"},
Title: "Belajar komposisi",
}
fmt.Println(t.CreatedBy, t.Title)
}
Gunakan field bernama ketika embedding membuat API ambigu. Komposisi eksplisit sering lebih mudah dibaca:
Code
type Service struct {
store TaskStore
}
5. Contoh: Service dengan Store
Code
package main
import (
"errors""fmt""strings"
)
type Task struct {
ID int
Title string
}
type TaskStore interface {
Save(Task) error
All() []Task
}
type MemoryStore struct {
tasks []Task
}
func(s *MemoryStore) Save(t Task) error {
s.tasks = append(s.tasks, t)
returnnil
}
func(s *MemoryStore) All() []Task {
returnappend([]Task(nil), s.tasks...)
}
type Service struct {
store TaskStore
}
funcNewService(store TaskStore) Service {
return Service{store: store}
}
func(s Service) Add(id int, title string) error {
title = strings.TrimSpace(title)
if id < 1 || title == "" {
return errors.New("ID dan judul tidak valid")
}
return s.store.Save(Task{ID: id, Title: title})
}
funcmain() {
store := &MemoryStore{}
service := NewService(store)
if err := service.Add(, ); err != {
(err)
}
fmt.Println(store.All())
}
All mengembalikan salinan slice agar pemanggil tidak dapat mengubah penyimpanan secara tidak sengaja.
Kesalahan Umum
Semua hal dijadikan interface. Mulailah dengan tipe konkret; ekstrak interface saat ada kebutuhan substitusi atau pengujian.
Receiver nilai untuk mutasi. Perubahan hanya terjadi pada salinan.
Interface terlalu besar. Pecah berdasarkan kebutuhan pemakai; satu sampai tiga method sering cukup.
Menganggap embedding sebagai inheritance. Embedded type tidak membentuk hierarki subtype.
Mengekspor seluruh field. Ekspor hanya bagian API yang memang diperlukan.
Menyalin struct yang mengandung mutex. Gunakan pointer dan jangan menyalin nilai sinkronisasi.
Nil pointer di dalam interface. Interface dapat tidak nil meskipun nilai konkret di dalamnya berupa pointer nil.
Latihan
Tambahkan Priority int pada Task dan method Validate() error dengan rentang 1–5.
Buat interface TaskReader yang hanya memiliki All() []Task.
Implementasikan FindByID(int) (Task, bool) pada MemoryStore.
Buat Logger kecil dengan method Printf(string, ...any) dan injeksikan ke Service.
Jelaskan kapan field bernama lebih baik daripada embedding pada contoh Metadata.
Ringkasan
struct memodelkan data; method menempatkan perilaku dekat dengan data.
Receiver pointer diperlukan untuk mutasi, sedangkan receiver nilai cocok untuk operasi baca pada nilai kecil.
Interface Go dipenuhi secara implisit dan sebaiknya kecil serta berpusat pada kebutuhan pemakai.
Komposisi menggantikan pewarisan dan menjaga hubungan antarkomponen tetap eksplisit.
// task/task.gopackage task
import (
"errors""strings"
)
type Task struct {
ID int
Title string
}
funcNew(id int, title string) (Task, error) {
title = strings.TrimSpace(title)
if id < 1 {
return Task{}, errors.New("ID harus positif")
}
if title == "" {
return Task{}, errors.New("judul tidak boleh kosong")
}
return Task{ID: id, Title: title}, nil
}
Import dikelompokkan oleh gofmt: pustaka standar, baris kosong, lalu package module atau pihak ketiga. Hindari dot import karena asal identifier menjadi tidak jelas.
3. Struktur Proyek yang Proporsional
Untuk aplikasi kecil, satu main.go dapat cukup. Pecah direktori setelah ada tanggung jawab nyata yang berbeda.
Direktori cmd/taskcli berisi entry point. internal/task menyimpan kode aplikasi yang tidak boleh diimpor module lain. Go menegakkan batas internal saat kompilasi.
Memuat diagram…
Jangan menyalin struktur proyek besar tanpa kebutuhan. Folder generik seperti utils, common, atau helpers sering menyembunyikan tanggung jawab yang tidak jelas.
4. Siklus Import
Go melarang import cycle. Jika package task mengimpor storage, maka storage tidak boleh kembali mengimpor task.
Solusi yang lazim:
pindahkan tipe domain ke package yang paling memilikinya;
definisikan interface kecil di package pemakai;
gabungkan package jika pemisahannya tidak memberi manfaat;
pindahkan kode bersama ke package ketiga hanya jika konsepnya benar-benar mandiri.
5. Mengelola Dependensi
go.mod mencatat path module, versi Go, dan kebutuhan module lain. go.sum mencatat checksum untuk verifikasi unduhan.
Code
go mod tidy
go list -m all
go mod graph
go mod verify
go mod tidy menambah dependensi yang diperlukan dan menghapus yang tidak dipakai. Jalankan setelah perubahan import.
Jika memang memerlukan package eksternal:
Code
go get example.com/some/module@v1.2.3
go mod tidy
Gunakan versi eksplisit bila proyek menuntut reproduksibilitas. Evaluasi lisensi, pemeliharaan, keamanan, dan ukuran transitive dependency sebelum menambah library. Pustaka standar sering sudah cukup.
Untuk memperbarui dependensi langsung dan tidak langsung sesuai batas kompatibilitas:
Code
go get -u ./...
go mod tidy
go test ./...
Tinjau perubahan go.mod dan go.sum; jangan memperbarui tanpa menjalankan pengujian.
6. Package Documentation dan API
Dokumentasi identifier yang diekspor dimulai dengan namanya.
Code
// Package task menyediakan model dan operasi tugas.package task
// Task merepresentasikan satu pekerjaan yang dapat diselesaikan.type Task struct {
ID int
}
API package yang baik memiliki permukaan kecil, nama jelas, dan aturan error yang konsisten. Hindari mengekspos representasi internal jika pengguna hanya memerlukan operasi tingkat tinggi.
// greeting/greeting.gopackage greeting
import"strings"funcHello(name string)string {
name = strings.TrimSpace(name)
if name == "" {
name = "dunia"
}
return"Halo, " + name + "!"
}
Mode 0o600 memberi akses baca-tulis kepada pemilik. Permission aktual dapat dipersempit oleh umask. Untuk data pengguna yang mungkin sensitif, jangan memakai mode yang dapat dibaca semua pengguna.
Untuk data besar atau streaming, gunakan os.Open, bufio.Scanner, bufio.Reader, atau io.Copy. File yang dibuka harus ditutup.
Jangan memperlakukan permission denied atau kerusakan disk sebagai “data kosong”; tindakan tersebut menyembunyikan masalah dan berpotensi menimpa data.
4. Penulisan Atomik
os.WriteFile langsung ke file tujuan dapat meninggalkan file terpotong bila proses berhenti. Pola yang lebih aman adalah menulis file sementara pada direktori yang sama, menyinkronkan, menutup, lalu mengganti nama.
Memuat diagram…
Program lengkap berikut menyediakan penyimpanan JSON sederhana:
Rename atomik secara umum jika sumber dan tujuan berada pada filesystem yang sama; karena itu file sementara dibuat di direktori tujuan. Jaminan detail tetap bergantung pada sistem operasi dan filesystem. Untuk ketahanan terhadap kehilangan daya yang lebih ketat, sinkronisasi direktori setelah rename dapat diperlukan.
5. Menentukan Lokasi Data CLI
Hindari menulis data tetap di current working directory karena direktori tersebut berubah sesuai cara program dijalankan. Gunakan direktori konfigurasi pengguna:
Izinkan flag seperti -file agar pengujian dan otomasi dapat memilih lokasi lain. Jangan membangun path dengan penggabungan string; gunakan filepath.Join.
6. Batas Persistensi JSON
JSON sesuai untuk CLI pengguna tunggal dengan data kecil. Ia bukan solusi ideal untuk banyak proses yang menulis bersamaan, query kompleks, atau dataset besar. Penulisan atomik mencegah file setengah jadi, tetapi tidak mencegah lost update antara dua proses. Pada kebutuhan tersebut, tambahkan locking yang sesuai platform atau beralih ke database transaksional.
Kesalahan Umum
Mengabaikan error I/O atau JSON. Selalu beri konteks dengan %w.
Menimpa file ketika decode gagal. Hentikan operasi dan laporkan kerusakan data.
Membuat temp file di filesystem lain.Rename dapat gagal dan kehilangan sifat atomik.
Permission terlalu longgar. Gunakan 0o600 untuk file data pribadi dan 0o700 untuk direktori.
Lupa menutup file. Gunakan defer untuk input; tangani error close pada output penting.
Mengandalkan current working directory. Gunakan direktori pengguna atau flag eksplisit.
Menganggap JSON sebagai database multiwriter. Tambahkan koordinasi atau gunakan penyimpanan transaksional.
Latihan
Tambahkan field CreatedAt time.Time dan amati representasi JSON-nya.
Gunakan json.Decoder dengan DisallowUnknownFields untuk memuat file.
Tulis fungsi Backup(path string) yang menyalin data lama sebelum perubahan.
Uji Load untuk file tidak ada, kosong, JSON rusak, dan JSON valid.
Tambahkan flag -file ke CLI kecil dan gunakan t.TempDir() dalam pengujian.
Ringkasan
Pilih API file sesuai ukuran dan pola akses data.
Struct tag mengontrol format JSON; semua kegagalan encode/decode harus ditangani.
Bedakan file tidak ada dari error I/O lain.
Temp-file, Sync, Close, dan Rename mengurangi risiko korupsi saat menyimpan.
JSON cocok untuk persistensi sederhana, bukan konkurensi penulis tanpa koordinasi.
// task.gopackage task
import (
"errors""strings"
)
type Task struct {
ID int
Title string
}
funcNew(id int, title string) (Task, error) {
title = strings.TrimSpace(title)
if id < 1 {
return Task{}, errors.New("ID harus positif")
}
if title == "" {
return Task{}, errors.New("judul tidak boleh kosong")
}
return Task{ID: id, Title: title}, nil
}
Code
// task_test.gopackage task
import"testing"funcTestNew(t *testing.T) {
got, err := New(1, " Belajar test ")
if err != nil {
t.Fatalf("New() error = %v", err)
}
if got.Title != "Belajar test" {
t.Errorf("Title = %q, ingin %q", got.Title, "Belajar test")
}
}
Gunakan Fatalf jika test tidak dapat dilanjutkan; gunakan Errorf jika pemeriksaan lain masih bermakna. Pesan kegagalan sebaiknya memuat nilai aktual dan nilai yang diharapkan.
2. Table-Driven Tests
Table-driven test mengumpulkan beberapa skenario dalam satu struktur data. Setiap kasus dijalankan sebagai subtest dengan t.Run.
Code
package task
import"testing"funcTestNewTable(t *testing.T) {
tests := []struct {
name string
id int
title string
wantTitle string
wantErr bool
}{
{name: "valid", id: 1, title: " Belajar Go ", wantTitle: "Belajar Go"},
{name: "ID nol", id: 0, title: "Belajar", wantErr: true},
{name: "judul kosong", id: 1, title: " ", wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := New(tt.id, tt.title)
if (err != nil) != tt.wantErr {
t.Fatalf("New(%d, %q) error = %v, wantErr %t", tt.id, tt.title, err, tt.wantErr)
}
if !tt.wantErr && got.Title != tt.wantTitle {
t.Errorf("Title = %q, ingin %q", got.Title, tt.wantTitle)
}
})
}
}
Nama kasus harus menjelaskan perilaku, bukan nomor urut. Jangan memakai table-driven test jika hanya ada satu skenario sederhana dan tabel justru mengaburkan maksud.
3. Helper dan Cleanup
t.Helper() membuat lokasi kegagalan menunjuk ke pemanggil helper. t.Cleanup() memastikan pembersihan dijalankan setelah test.
Test yang baik memberikan hasil sama pada setiap eksekusi. Hindari ketergantungan pada:
waktu nyata tanpa cara mengendalikannya;
urutan iterasi map;
jaringan eksternal;
direktori kerja atau file pengguna;
state global yang dibagi antartest.
Jika logika membutuhkan waktu, injeksikan fungsi atau nilai waktu. Jika keluaran berasal dari map, urutkan key sebelum membandingkan. Jalankan t.Parallel() hanya setelah memastikan tidak ada state bersama yang berubah.
Memuat diagram…
5. Membandingkan Error
Jangan membandingkan teks error jika kode menyediakan sentinel error atau tipe khusus.
Code
var ErrNotFound = errors.New("tugas tidak ditemukan")
if !errors.Is(err, ErrNotFound) {
t.Fatalf("error = %v, ingin ErrNotFound", err)
}
Gunakan errors.As untuk memeriksa tipe error. Pemeriksaan string rapuh karena perubahan redaksi dapat merusak test tanpa mengubah perilaku.
6. Coverage
Coverage menunjukkan bagian statement yang dieksekusi, bukan kualitas pengujian.
Code
go test ./...
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go tool cover -html=coverage.out
Coverage tinggi tetap dapat melewatkan assertion penting, error path, race, atau kebutuhan bisnis. Gunakan laporan untuk menemukan area yang belum diuji, bukan sebagai satu-satunya target kualitas.
7. Race Detector
Data race terjadi ketika goroutine mengakses lokasi memori yang sama secara bersamaan, setidaknya satu akses adalah penulisan, dan tidak ada sinkronisasi yang benar.
Code
package counter
import"sync"type Counter struct {
mu sync.Mutex
n int
}
func(c *Counter) Add() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
func(c *Counter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.n
}
Code
package counter
import (
"sync""testing"
)
funcTestCounterConcurrent(t *testing.T) {
var c Counter
var wg sync.WaitGroup
forrange100 {
wg.Add(1)
gofunc() {
defer wg.Done()
c.Add()
}()
}
wg.Wait()
if got := c.Value(); got != 100 {
t.Fatalf("Value() = %d, ingin 100", got)
}
}
Code
go test -race ./...
Race detector hanya menemukan race pada jalur yang benar-benar dijalankan. Ia menambah waktu dan memori, tetapi sangat bernilai pada CI atau sebelum rilis. Race detector bukan pengganti desain sinkronisasi dan tidak mendeteksi semua logical race.
8. Perintah Verifikasi Praktis
Code
gofmt -w .
go vet ./...
go test ./...
go test -shuffle=on -count=10 ./...
go test -race ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
-shuffle=on membantu menemukan ketergantungan urutan. -count=10 membantu mengungkap test yang sesekali gagal, tetapi bukan bukti mutlak bahwa test bebas flakiness.
Kesalahan Umum
Hanya menguji happy path. Sertakan input batas, error, dan data rusak.
Assertion tanpa pesan diagnostik. Tampilkan aktual, harapan, dan input relevan.
Test memakai file pengguna. Gunakan t.TempDir().
Mengejar 100% coverage tanpa nilai. Prioritaskan risiko dan perilaku penting.
Memakai t.Parallel() pada state bersama. Isolasi atau sinkronkan terlebih dahulu.
Mengabaikan hasil -race. Race adalah bug; perbaiki, jangan sekadar menonaktifkan pemeriksaan.
Sleep untuk sinkronisasi. Gunakan channel, WaitGroup, atau primitive sinkronisasi lain.
Latihan
Tambahkan kasus batas pada TestNewTable, termasuk whitespace Unicode.
Tulis test Load untuk file valid, tidak ada, kosong, dan JSON rusak menggunakan t.TempDir().
Buat helper fixture dengan t.Helper().
Sengaja hapus mutex dari Counter, jalankan go test -race, lalu perbaiki kembali.
Buat laporan coverage dan identifikasi satu error path yang belum diuji.
Ringkasan
Package testing menyediakan unit test tanpa framework tambahan.
Table-driven tests efektif untuk banyak variasi perilaku yang sama.
Helper, t.TempDir, dan input terkendali menjaga test diagnostik serta deterministik.
Coverage mengukur eksekusi statement, bukan kebenaran.
Race detector menemukan akses memori tidak tersinkronisasi pada jalur yang diuji.
main menangani argumen, output, dan exit code. Package task memiliki aturan aplikasi. JSONStore menangani file. Interface didefinisikan di sisi pemakai agar kontraknya kecil.
mkdir taskcli
cd taskcli
go mod init example.com/taskcli
mkdir -p cmd/task internal/task
4. Model Domain
Code
// internal/task/task.gopackage task
import"errors"const MaxTitleBytes = 200var (
ErrEmptyTitle = errors.New("judul tidak boleh kosong")
ErrLongTitle = errors.New("judul terlalu panjang")
ErrNotFound = errors.New("tugas tidak ditemukan")
)
type Task struct {
ID int`json:"id"`
Title string`json:"title"`
Done bool`json:"done"`
}
Sentinel error memungkinkan pemanggil menggunakan errors.Is. Batas byte sederhana dan deterministik. Jika kebutuhan berubah menjadi batas karakter pengguna, gunakan perhitungan rune dan tetapkan aturan Unicode dengan jelas.
Aplikasi melakukan load-modify-save. Desain ini cukup untuk satu proses dan data kecil. Dua proses yang menulis bersamaan dapat mengalami lost update; tambahkan file locking atau database ketika multiwriter menjadi kebutuhan nyata.
Logika run dapat diuji tanpa menjalankan subprocess karena menerima argumen dan writer. main tetap kecil dan menjadi satu-satunya tempat yang memanggil os.Exit.
Tambahkan test untuk save error, load error, ID maksimum, delete, file tidak ada, dan argumen CLI. Test run dapat memakai bytes.Buffer sebagai stdout dan stderr.
9. Validasi dan Penanganan Error
Validasi dilakukan pada dua batas:
CLI: jumlah argumen, nama perintah, dan format ID.
Domain: judul kosong/panjang dan keberadaan tugas.
Error I/O dibungkus memakai %w agar konteks terbaca tanpa kehilangan penyebab. Jangan mencetak error pada setiap lapisan; kembalikan error sampai main, lalu cetak sekali ke stderr. Output normal masuk stdout agar dapat dipipe.
Exit code versi ini:
0: operasi berhasil;
1: input, domain, atau operasi gagal.
Jika otomasi nantinya perlu membedakan kesalahan penggunaan dari kegagalan penyimpanan, petakan error ke exit code khusus di main. Jangan menambahkan klasifikasi sebelum ada konsumen nyata.
Flag global harus muncul sebelum subcommand karena flag.FlagSet berhenti memproses flag pada argumen non-flag pertama.
11. Perintah Verifikasi
Jalankan seluruh pemeriksaan dari root module:
Code
gofmt -w .
go vet ./...
go test ./...
go test -shuffle=on -count=10 ./...
go test -race ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go build ./cmd/task
go mod tidy
go mod verify