Mesin hitung dari Bagian 2 percaya sepenuhnya pada pemanggilnya. Sayangnya, pengguna terminal tidak selalu mengetik dengan benar: operasi bisa salah ketik dan angka bisa berupa teks. Pada bagian ini kita membangun penjaga pintu bernama ParseInput, menyambungkannya ke titik masuk program, dan menerjemahkan setiap jenis kegagalan menjadi exit code yang terdokumentasi.

Memvalidasi Nama Operasi #

Validasi paling sederhana adalah memeriksa apakah nama operasi termasuk salah satu dari empat operasi yang dikenal:

go
// 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
	}
}

switch tanpa nilai pembanding pada baris 39 memeriksa operasi terhadap empat literal string dalam satu cabang case, lalu mengembalikan bool yang mudah dipakai dalam kondisi if. Pencocokannya bersifat case-sensitive — TAMBAH ditolak karena berbeda dengan tambah, keputusan yang kita uji eksplisit pada Bagian 4.

ParseInput: Satu Pintu Validasi #

Alih-alih memvalidasi di banyak tempat, seluruh pemeriksaan dikumpulkan dalam satu fungsi yang menerima teks mentah dari pengguna dan mengembalikan nilai yang sudah bersih:

go
// 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
}

Mari kita telaah baris demi baris:

  1. Baris 50 (func ParseInput(operasi, teksA, teksB string) (float64, float64, error)): tiga teks masuk, dua angka dan satu error keluar — bentuk yang membuat jalur gagal sulit diabaikan pemanggil.
  2. Baris 51-53 (if !OperasiValid(operasi)): nama operasi diperiksa pertama kali. fmt.Errorf dengan verb %q mencetak string dengan tanda kutip sehingga string kosong atau spasi tersembunyi langsung terlihat di pesan error.
  3. Baris 55-58 (strconv.ParseFloat(teksA, 64)): teks angka pertama diubah menjadi float64. Bila gagal, error asli dari strconv dibungkus dengan %w — bukan %v — sehingga rantai error tetap utuh.
  4. Baris 60-63: pemeriksaan yang sama diulang untuk angka kedua dengan pesan yang menyebut posisinya, sehingga pengguna tahu angka mana yang salah.
  5. Baris 65 (return a, b, nil): hanya satu jalur sukses — semua pemeriksaan sudah lolos.

Membaca Error Berlapis #

Pembungkusan dengan %w membuat satu baris error memuat tiga lapis informasi. Jalankan program dengan angka yang bukan angka:

bash
go run . tambah abc 3

Output yang Dihasilkan (stderr):

text
Input tidak valid: angka pertama "abc" tidak valid: strconv.ParseFloat: parsing "abc": invalid syntax

Baris tersebut dibaca dari kiri: label dari main.go menjelaskan kategori kegagalan, pesan dari ParseInput menunjuk angka pertama, dan pesan asli strconv.ParseFloat menjelaskan penyebab teknisnya. Karena rantai error tidak diputus, test pada Bagian 4 dapat memeriksanya hingga akar dengan errors.Is.

Titik Masuk dan Kontrak Exit Code #

main.go adalah satu-satunya berkas yang menyentuh dunia luar. Bagian awalnya mendeklarasikan kontrak exit code sebagai konstanta bernama:

go
// 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:]))
}

Dua keputusan penting ada di sini. Pertama, konstanta bernama menggantikan angka ajaib — pembaca kode tidak perlu menebak makna 2. Kedua, os.Exit dibungkus di luar jalankan agar defer di dalam jalankan tetap berjalan sebelum program berhenti. Fungsi jalankan sendiri berjalan dalam empat tahap yang mencerminkan alur desain dari Bagian 1:

go
// 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
}
  1. Baris 24-28: jumlah argumen diperiksa lebih dulu. Format perintah yang salah mendapat exit code 1 lewat konstanta exitPenggunaan, dan pesannya ditulis ke os.Stderr.
  2. Baris 30-34: ParseInput menerima ketiga argumen mentah. Kegagalan validasi mendapat exit code 2 lewat exitSalahInput.
  3. Baris 36-40: fungsi hitung menjalankan operasi. Untuk bagi, error ErrPembagianNol muncul di sini dan juga dilaporkan dengan exit code 2 — masukan 0 sebagai penyebut memang input yang tidak valid.
  4. Baris 42-43: jalur sukses mencetak Hasil ke standard output dengan format %g lalu mengembalikan exitSukses.

hitung sendiri hanyalah pemetaan nama operasi ke fungsi dari Bagian 2:

go
// 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)
	}
}

Cabang default pada baris 57-58 tidak akan pernah tercapai karena ParseInput sudah menyaring operasi tak dikenal. Cabang tersebut tetap berguna sebagai pengaman bila kelak ada pemanggil baru yang melewati validasi.

Sesi Uji Kesalahan #

Bangun biner lalu saksikan seluruh kontrak bekerja. Tiga jenis kegagalan menghasilkan tiga perilaku yang berbeda:

bash
go build -o kalkulator .
./kalkulator tambah abc 3
echo "exit code: $?"
./kalkulator bagi 10 0
echo "exit code: $?"
./kalkulator tambah 2
echo "exit code: $?"

Output yang Dihasilkan:

text
Input tidak valid: angka pertama "abc" tidak valid: strconv.ParseFloat: parsing "abc": invalid syntax
exit code: 2
Galat perhitungan: pembagian dengan nol
exit code: 2
Penggunaan: kalkulator <operasi> <angka1> <angka2>
Operasi yang tersedia: tambah, kurang, kali, bagi
exit code: 1

Baris pertama dan kedua berasal dari stderr dengan exit code 2 karena masukan tidak valid; baris ketiga menunjukkan kesalahan format dengan exit code 1. Seluruh pesan kesalahan mengalir ke stderr, sehingga ./kalkulator tambah 2 3 > hasil.txt menghasilkan berkas yang hanya berisi hasil bersih.

Kode lengkap main.go, calculator.go, beserta seluruh berkas proyek tersedia pada bagian Berkas Sumber di bawah halaman ini.

Ringkasan Bagian 3 #

Validasi kini menjadi satu pintu bernama ParseInput, error asli strconv terbawa utuh berkat %w, dan main.go menerjemahkan kegagalan menjadi exit code yang terdokumentasi. Tinggal membuktikan bahwa semua janji ini dipegang oleh test pada Bagian 4.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

calculator_test.go

Unduh berkas
go
package 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 berkas
go
package 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 berkas
go
package 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)
	}
}