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:
// 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:
// 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:
- Baris 50 (
func ParseInput(operasi, teksA, teksB string) (float64, float64, error)): tiga teks masuk, dua angka dan satuerrorkeluar — bentuk yang membuat jalur gagal sulit diabaikan pemanggil. - Baris 51-53 (
if !OperasiValid(operasi)): nama operasi diperiksa pertama kali.fmt.Errorfdengan verb%qmencetak string dengan tanda kutip sehingga string kosong atau spasi tersembunyi langsung terlihat di pesan error. - Baris 55-58 (
strconv.ParseFloat(teksA, 64)): teks angka pertama diubah menjadifloat64. Bila gagal, error asli daristrconvdibungkus dengan%w— bukan%v— sehingga rantai error tetap utuh. - Baris 60-63: pemeriksaan yang sama diulang untuk angka kedua dengan pesan yang menyebut posisinya, sehingga pengguna tahu angka mana yang salah.
- 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:
go run . tambah abc 3Output yang Dihasilkan (stderr):
Input tidak valid: angka pertama "abc" tidak valid: strconv.ParseFloat: parsing "abc": invalid syntaxBaris 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:
// 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:
// 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
}- Baris 24-28: jumlah argumen diperiksa lebih dulu. Format perintah yang salah mendapat exit code
1lewat konstantaexitPenggunaan, dan pesannya ditulis keos.Stderr. - Baris 30-34:
ParseInputmenerima ketiga argumen mentah. Kegagalan validasi mendapat exit code2lewatexitSalahInput. - Baris 36-40: fungsi
hitungmenjalankan operasi. Untukbagi, errorErrPembagianNolmuncul di sini dan juga dilaporkan dengan exit code2— masukan0sebagai penyebut memang input yang tidak valid. - Baris 42-43: jalur sukses mencetak
Hasilke standard output dengan format%glalu mengembalikanexitSukses.
hitung sendiri hanyalah pemetaan nama operasi ke fungsi dari Bagian 2:
// 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:
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:
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: 1Baris 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 #
- Dokumentasi package strconv
— perilaku
ParseFloatdan errorErrSyntax. - Error wrapping pada Go 1.13
— artikel resmi tentang
%wdanerrors.Is. - Format verb fmt
— daftar resmi verb
%q,%g, dan%w.
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)
}
}