Program yang selesai cepat biasanya bukan yang langsung diketik, melainkan yang dirancang dulu di atas kertas. Pada bagian ini kita memutuskan empat hal sebelum menulis satu baris kode pun: operasi apa saja yang didukung, tipe data apa yang menampung angka, bagaimana input pengguna mengalir dari command line hingga menjadi hasil, dan berkas apa saja yang dibutuhkan proyek. Keputusan-keputusan ini menjadi kerangka yang diisi tiga bagian berikutnya.

Operasi yang Didukung #

Kalkulator kita mendukung empat operasi aritmatika dasar dengan bentuk perintah yang seragam:

OperasiBentuk matematisContoh perintah
tambaha + bkalkulator tambah 2 3
kuranga - bkalkulator kurang 10 4
kalia × bkalkulator kali 6 7
bagia ÷ bkalkulator bagi 10 4

Empat operasi ini dipilih bukan hanya karena umum, tetapi juga karena mewakili dua kategori fungsi yang berbeda. tambah, kurang, dan kali selalu berhasil untuk masukan apa pun, sedangkan bagi bisa gagal ketika penyebutnya nol. Perbedaan kategori ini menentukan bentuk tanda tangan fungsinya: tiga operasi pertama cukup mengembalikan angka, sementara bagi juga mengembalikan error.

Memilih Tipe Data untuk Angka #

Command line selalu menyerahkan argumen sebagai teks, jadi program harus mengubah teks "2" menjadi angka. Kita memilih float64 sebagai tipe data tunggal untuk seluruh operasi dengan dua alasan. Pertama, strconv.ParseFloat menerima bentuk masukan yang luas: "2", "2.5", "-8", hingga "1e3" semuanya berhasil diparse. Kedua, pembagian seperti 10 ÷ 4 menghasilkan 2.5 yang tidak tertampung dalam int.

Alur Pemrosesan Input #

Setiap perintah pengguna melewati jalur yang sama dari awal hingga akhir. Diagram berikut memetakan alur tersebut lengkap dengan tiga kemungkinan akhir:

Mari kita uraikan tahapannya. Pertama, program memeriksa jumlah argumen: bentuk perintah yang salah (misalnya kurang satu angka) langsung ditolak dengan exit code 1 tanpa memproses apa pun. Kedua, fungsi ParseInput memvalidasi nama operasi dan mengubah kedua teks angka menjadi float64; kegagalan pada tahap ini menghasilkan exit code 2. Ketiga, operasi dijalankan; hanya bagi yang dapat gagal pada tahap ini, yaitu ketika penyebutnya nol, dan kegagalan tersebut juga dilaporkan dengan exit code 2. Terakhir, hasil dicetak ke standard output dan program selesai dengan exit code 0.

Pemisahan jalur keluaran juga menjadi bagian desain: pesan keberhasilan ditulis ke stdout, sedangkan seluruh pesan galat ditulis ke stderr. Pemisahan ini memungkinkan skrip lain menyaring hasil perhitungan tanpa tercemar pesan kesalahan.

Struktur Module dan Berkas #

Proyek berdiri sebagai satu module mandiri dengan nama example.com/kalkulator-terminal. Berkas go.mod berisi dua baris:

text
module example.com/kalkulator-terminal

go 1.27

Struktur direktori proyek:

text
kalkulator-terminal/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
└── README.md

Setiap berkas memegang satu tanggung jawab: calculator.go berisi operasi hitung dan validasi yang murni bekerja dengan nilai, main.go menjadi satu-satunya tempat yang menyentuh os.Args, os.Stdout, os.Stderr, dan os.Exit, sedangkan calculator_test.go menguji fungsi-fungsi di calculator.go. Seluruh berkas berada dalam satu package main — untuk proyek sekecil ini, pemecahan package hanya menambah biaya tanpa manfaat.

Kode lengkap seluruh berkas proyek dapat dilihat pada bagian Berkas Sumber di bagian bawah halaman ini.

Ringkasan Bagian 1 #

Desain telah lengkap: empat operasi dengan dua kategori kegagalan, float64 sebagai tipe angka tunggal, alur input yang bercabang ke tiga exit code, dan tiga berkas dengan tanggung jawab terpisah. Pada Bagian 2 kita mengisi berkas pertama: empat operasi aritmatika sebagai fungsi murni beserta sentinel error untuk pembagian nol.

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