Kalkulator terminal adalah proyek pertama yang pas: cukup kecil untuk diselesaikan cepat, cukup nyata untuk melatih parsing argument, validasi input, pembagian tanggung jawab fungsi, dan penulisan test. Kita akan mendesain operasi aritmatika sebagai fungsi murni, memvalidasi input pengguna, lalu menguji semuanya dengan table-driven test.
Seluruh kode sumber proyek tersedia pada bagian Berkas Sumber di bawah halaman ini, terdiri atas satu module Go tanpa dependency eksternal. Setiap bagian pengerjaan menampilkan potongan kode dari berkas yang sama, sehingga kondisi proyek tetap utuh di setiap tahap.
Perilaku yang Diharapkan #
Program menerima satu operasi dan dua angka, lalu mencetak hasil perhitungan:
kalkulator <operasi> <angka1> <angka2>Empat operasi yang didukung beserta contohnya:
| Operasi | Contoh perintah | Hasil |
|---|---|---|
tambah | kalkulator tambah 2 3 | 5 |
kurang | kalkulator kurang 10 4 | 6 |
kali | kalkulator kali 6 7 | 42 |
bagi | kalkulator bagi 10 4 | 2.5 |
Kedua angka dibaca sebagai teks dari command line, diubah menjadi float64 melalui strconv.ParseFloat, dihitung, lalu hasilnya dicetak ke standard output dengan format %g yang rapi untuk bilangan bulat maupun desimal.
Kontrak Exit Code #
Program CLI yang baik melaporkan hasil eksekusi melalui exit code, bukan hanya teks. Skrip lain atau pipeline dapat memeriksa angka ini untuk mengetahui apakah perintah berhasil:
| Exit code | Arti | Contoh pemicu |
|---|---|---|
0 | Perhitungan berhasil | kalkulator tambah 2 3 |
1 | Format perintah salah | jumlah argumen tidak sama dengan tiga |
2 | Input tidak valid | operasi tak dikenal, angka tak valid, pembagian dengan nol |
Ringkasan Arsitektur #
Proyek ini memakai satu package main dengan berkas yang dipilah berdasarkan tanggung jawab:
| Berkas | Tanggung jawab |
|---|---|
main.go | Membaca argument, memanggil validasi, mendispatch operasi, menentukan exit code |
calculator.go | Operasi aritmatika murni, sentinel error pembagian nol, fungsi validasi input |
calculator_test.go | Table-driven test, subtest, dan example test untuk seluruh fungsi di atas |
Pemisahan ini membuat operasi hitung dapat diuji tanpa menjalankan command line sama sekali: test hanya memanggil Tambah, Bagi, dan ParseInput sebagai fungsi biasa. Sebaliknya, seluruh kode yang menyentuh os.Args dan os.Exit terkumpul di main.go sehingga batas antara logika dan antarmuka tetap terlihat jelas. Karena proyek masih kecil, kita sengaja tidak memecahnya menjadi beberapa package — satu package main sudah cukup dan menyederhanakan struktur.
Prasyarat #
Sebelum mulai, pastikan Anda sudah membaca chapter berikut:
- Tipe Data Numerik
— dasar
float64dan jangkauannya. - Mendefinisikan Fungsi
dan Multiple Return Values
— bentuk fungsi yang dipakai
BagidanParseInput. - Sentinel Error
— pola
ErrPembagianNolyang dipakai proyek ini. - Table-Driven Test — pola test yang dipakai pada bagian pengujian.
Menjalankan Proyek #
Dari direktori proyek yang berisi go.mod, jalankan program langsung tanpa kompilasi terpisah:
go run . tambah 2 3Output yang Dihasilkan:
Hasil: 5Bangun biner mandiri bila ingin memakai program berulang kali, lalu jalankan tesnya:
go build -o kalkulator .
./kalkulator bagi 10 4
go test ./...Tahapan Pengerjaan #
Proyek dikerjakan dalam empat bagian berurutan. Setiap bagian menghasilkan program yang tetap dapat dijalankan:
- Bagian 1: Desain Program — merancang operasi, tipe data, alur pemrosesan input, dan kontrak exit code.
- Bagian 2: Implementasi Operasi — menulis empat operasi aritmatika sebagai fungsi murni beserta sentinel error.
- Bagian 3: Validasi Input — memvalidasi operasi dan angka, lalu menyambungkannya ke titik masuk dan exit code.
- Bagian 4: Pengujian Kalkulator — menguji seluruh fungsi dengan table-driven test, subtest, dan example test.
Mulailah dari Bagian 1 dan ikuti urutannya karena setiap bagian dibangun di atas keputusan bagian sebelumnya.
Rancang struktur kalkulator: operasi, tipe data, dan alur pemrosesan input.
Buka → # Bagian 2: Implementasi OperasiImplementasikan operasi aritmatika sebagai fungsi yang mudah diuji.
Buka → # Bagian 3: Validasi InputValidasi angka dan operator dari pengguna dengan pesan error yang jelas.
Buka → # Bagian 4: Pengujian KalkulatorUji kalkulator dengan table-driven test dan example test.
Buka →Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
calculator_test.go
Unduh berkaspackage main
import (
"errors"
"fmt"
"strconv"
"strings"
"testing"
)
func TestTambah(t *testing.T) {
cases := []struct {
nama string
a, b, want float64
}{
{"dua bilangan bulat", 2, 3, 5},
{"dua bilangan negatif", -4, -6, -10},
{"campuran positif negatif", 7, -2, 5},
{"bilangan desimal", 1.5, 2.25, 3.75},
}
for _, c := range cases {
t.Run(c.nama, func(t *testing.T) {
if got := Tambah(c.a, c.b); got != c.want {
t.Errorf("Tambah(%g, %g) = %g; want %g", c.a, c.b, got, c.want)
}
})
}
}
func TestKurang(t *testing.T) {
cases := []struct {
a, b, want float64
}{
{10, 4, 6},
{2, 5, -3},
{-1.5, -2.5, 1},
}
for _, c := range cases {
if got := Kurang(c.a, c.b); got != c.want {
t.Errorf("Kurang(%g, %g) = %g; want %g", c.a, c.b, got, c.want)
}
}
}
func TestKali(t *testing.T) {
cases := []struct {
a, b, want float64
}{
{6, 7, 42},
{-3, 4, -12},
{0.5, 8, 4},
{0, 99, 0},
}
for _, c := range cases {
if got := Kali(c.a, c.b); got != c.want {
t.Errorf("Kali(%g, %g) = %g; want %g", c.a, c.b, got, c.want)
}
}
}
func TestBagi(t *testing.T) {
cases := []struct {
nama string
a, b, want float64
wantErrSent error
}{
{"hasil desimal", 10, 4, 2.5, nil},
{"hasil negatif", -9, 3, -3, nil},
{"dibagi satu", 7.5, 1, 7.5, nil},
{"pembagian dengan nol", 10, 0, 0, ErrPembagianNol},
}
for _, c := range cases {
t.Run(c.nama, func(t *testing.T) {
got, err := Bagi(c.a, c.b)
if c.wantErrSent != nil {
if !errors.Is(err, c.wantErrSent) {
t.Fatalf("Bagi(%g, %g) error = %v; want %v", c.a, c.b, err, c.wantErrSent)
}
return
}
if err != nil {
t.Fatalf("Bagi(%g, %g) error tak terduga: %v", c.a, c.b, err)
}
if got != c.want {
t.Errorf("Bagi(%g, %g) = %g; want %g", c.a, c.b, got, c.want)
}
})
}
}
func TestOperasiValid(t *testing.T) {
for _, operasi := range []string{"tambah", "kurang", "kali", "bagi"} {
if !OperasiValid(operasi) {
t.Errorf("OperasiValid(%q) = false; want true", operasi)
}
}
for _, operasi := range []string{"", "pangkat", "TAMBAH", "akar"} {
if OperasiValid(operasi) {
t.Errorf("OperasiValid(%q) = true; want false", operasi)
}
}
}
func TestParseInput(t *testing.T) {
cases := []struct {
nama string
operasi, teksA string
teksB string
wantA, wantB float64
wantErrSepenggal string
}{
{"input lengkap", "tambah", "2", "3", 2, 3, ""},
{"angka desimal", "bagi", "10", "2.5", 10, 2.5, ""},
{"angka negatif", "kurang", "-8", "2", -8, 2, ""},
{"operasi tidak dikenal", "pangkat", "2", "3", 0, 0, "tidak dikenal"},
{"angka pertama salah", "tambah", "abc", "3", 0, 0, "angka pertama"},
{"angka kedua salah", "bagi", "10", "x", 0, 0, "angka kedua"},
}
for _, c := range cases {
t.Run(c.nama, func(t *testing.T) {
a, b, err := ParseInput(c.operasi, c.teksA, c.teksB)
if c.wantErrSepenggal == "" {
if err != nil {
t.Fatalf("ParseInput(%q, %q, %q) error tak terduga: %v", c.operasi, c.teksA, c.teksB, err)
}
if a != c.wantA || b != c.wantB {
t.Errorf("ParseInput(%q, %q, %q) = (%g, %g); want (%g, %g)",
c.operasi, c.teksA, c.teksB, a, b, c.wantA, c.wantB)
}
return
}
if err == nil || !strings.Contains(err.Error(), c.wantErrSepenggal) {
t.Errorf("ParseInput(%q, %q, %q) error = %v; want pesan memuat %q",
c.operasi, c.teksA, c.teksB, err, c.wantErrSepenggal)
}
})
}
}
func TestParseInputMembungkusErrSyntax(t *testing.T) {
_, _, err := ParseInput("tambah", "abc", "3")
if !errors.Is(err, strconv.ErrSyntax) {
t.Fatalf("ParseInput(\"tambah\", \"abc\", \"3\") error = %v; want error yang membungkus strconv.ErrSyntax", err)
}
}
func ExampleTambah() {
fmt.Println(Tambah(2, 3))
// Output: 5
}
func ExampleBagi() {
hasil, err := Bagi(10, 4)
if err != nil {
fmt.Println("galat:", err)
return
}
fmt.Println(hasil)
// Output: 2.5
}
func ExampleBagi_nol() {
_, err := Bagi(10, 0)
fmt.Println("galat:", err)
// Output: galat: pembagian dengan nol
}
calculator.go
Unduh berkaspackage main
import (
"errors"
"fmt"
"strconv"
)
// ErrPembagianNol adalah sentinel error untuk percobaan pembagian dengan nol.
// Pemanggil cukup memeriksa errors.Is(err, ErrPembagianNol), bukan membandingkan teks pesan.
var ErrPembagianNol = errors.New("pembagian dengan nol")
// Tambah mengembalikan hasil penjumlahan a dan b.
func Tambah(a, b float64) float64 {
return a + b
}
// Kurang mengembalikan hasil pengurangan a dengan b.
func Kurang(a, b float64) float64 {
return a - b
}
// Kali mengembalikan hasil perkalian a dan b.
func Kali(a, b float64) float64 {
return a * b
}
// Bagi mengembalikan hasil pembagian a dengan b.
// Bila b bernilai nol, fungsi mengembalikan ErrPembagianNol.
func Bagi(a, b float64) (float64, error) {
if b == 0 {
return 0, ErrPembagianNol
}
return a / b, nil
}
// OperasiValid melaporkan apakah nama operasi dikenal oleh kalkulator.
func OperasiValid(operasi string) bool {
switch operasi {
case "tambah", "kurang", "kali", "bagi":
return true
default:
return false
}
}
// ParseInput memvalidasi operasi dan dua teks angka dari pengguna.
// Fungsi mengembalikan kedua angka sebagai float64, atau error yang
// menjelaskan bagian masukan mana yang salah.
func ParseInput(operasi, teksA, teksB string) (float64, float64, error) {
if !OperasiValid(operasi) {
return 0, 0, fmt.Errorf("operasi %q tidak dikenal; gunakan tambah, kurang, kali, atau bagi", operasi)
}
a, err := strconv.ParseFloat(teksA, 64)
if err != nil {
return 0, 0, fmt.Errorf("angka pertama %q tidak valid: %w", teksA, err)
}
b, err := strconv.ParseFloat(teksB, 64)
if err != nil {
return 0, 0, fmt.Errorf("angka kedua %q tidak valid: %w", teksB, err)
}
return a, b, nil
}
main.go
Unduh berkaspackage main
import (
"fmt"
"os"
)
// Kontrak exit code program. Nilai ini dibaca oleh skrip lain
// atau pipeline yang memanggil kalkulator.
const (
exitSukses = 0 // perhitungan berhasil, hasil tercetak ke stdout
exitPenggunaan = 1 // format perintah salah
exitSalahInput = 2 // operasi tak dikenal, angka tak valid, atau bagi dengan nol
)
func main() {
// os.Exit dipanggil di luar jalankan agar defer tetap bekerja
// bila kelak ditambahkan di dalam jalankan.
os.Exit(jalankan(os.Args[1:]))
}
// jalankan memproses argumen program dan mengembalikan exit code.
func jalankan(args []string) int {
if len(args) != 3 {
fmt.Fprintln(os.Stderr, "Penggunaan: kalkulator <operasi> <angka1> <angka2>")
fmt.Fprintln(os.Stderr, "Operasi yang tersedia: tambah, kurang, kali, bagi")
return exitPenggunaan
}
a, b, err := ParseInput(args[0], args[1], args[2])
if err != nil {
fmt.Fprintln(os.Stderr, "Input tidak valid:", err)
return exitSalahInput
}
hasil, err := hitung(args[0], a, b)
if err != nil {
fmt.Fprintln(os.Stderr, "Galat perhitungan:", err)
return exitSalahInput
}
fmt.Printf("Hasil: %g\n", hasil)
return exitSukses
}
// hitung menjalankan operasi yang diminta terhadap a dan b.
func hitung(operasi string, a, b float64) (float64, error) {
switch operasi {
case "tambah":
return Tambah(a, b), nil
case "kurang":
return Kurang(a, b), nil
case "kali":
return Kali(a, b), nil
case "bagi":
return Bagi(a, b)
default:
return 0, fmt.Errorf("operasi %q tidak dikenal", operasi)
}
}