Mesin hitung kalkulator berada di calculator.go. Berkas ini sengaja tidak mengandung satu baris pun kode antarmuka: tidak ada pembacaan os.Args, tidak ada pencetakan ke layar. Seluruh isinya hanyalah fungsi yang menerima nilai dan mengembalikan nilai — bentuk yang paling mudah diuji. Pada bagian ini kita menulis empat operasi aritmatika beserta error khusus untuk pembagian nol, lalu membuktikan semuanya bekerja dari terminal.

Tiga Operasi yang Selalu Berhasil #

tambah, kurang, dan kali tidak punya cara untuk gagal: berapapun dua float64 yang masuk, hasilnya selalu terdefinisi. Ketiganya karenanya ditulis sebagai fungsi satu baris yang mengembalikan float64 tanpa error:

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

Mari kita telaah pola ketiganya:

  1. Baris 14 (func Tambah(a, b float64) float64): tanda tangan fungsi menyatakan kontraknya — dua parameter float64 masuk, satu float64 keluar. Tidak ada error pada nilai balik karena operasi ini tidak memiliki kondisi gagal.
  2. Baris 15 (return a + b): isi fungsi menghitung hasil lalu mengembalikannya secara langsung. Tidak ada variabel sementara, tidak ada efek samping, tidak ada keadaan yang tersimpan.
  3. Baris 18-21 dan 23-26 (Kurang dan Kali): kedua fungsi mengikuti pola yang sama persis dengan operator yang berbeda.

Fungsi seperti ini disebut fungsi murni: hasilnya hanya ditentukan oleh argumennya, dan pemanggilan berulang dengan argumen yang sama selalu memberikan hasil yang sama. Purity inilah yang membuat test pada Bagian 4 ditulis tanpa persiapan apa pun.

Bagi yang Bisa Gagal dan Sentinel Error #

Pembagian berbeda: 10 ÷ 0 tidak memiliki hasil matematis. Perlu diketahui, Go tidak memanggil panic untuk pembagian float64 dengan nol — mengikuti standar IEEE 754, hasilnya adalah +Inf. Nilai +Inf tidak bermakna bagi pengguna kalkulator, jadi kita menolak masukan tersebut secara eksplisit dan mengembalikan error agar pemanggil yang memutuskan tindakannya:

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

Baris 11 mendeklarasikan ErrPembagianNol sebagai sentinel error: satu nilai error tingkat package yang menjadi penanda resmi kondisi pembagian nol. Dengan penanda tetap ini, pemanggil cukup memeriksa errors.Is(err, ErrPembagianNol) dan tidak pernah membandingkan teks pesannya. Fungsi Bagi memakai sentinel tersebut:

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

Mari kita bedah baris demi baris:

  1. Baris 30 (func Bagi(a, b float64) (float64, error)): dua nilai balik — hasil perhitungan dan kemungkinan error — adalah pola standar Go untuk operasi yang bisa gagal.
  2. Baris 31-33 (if b == 0 ...): pemeriksaan penyebut nol berdiri paling depan. Bila kondisi terpenuhi, fungsi mengembalikan 0 sebagai hasil pengganti dan ErrPembagianNol sebagai penanda kegagalan.
  3. Baris 34 (return a / b, nil): jalur normal mengembalikan hasil pembagian dengan nil sebagai error. Angka 0 yang dikembalikan pada jalur gagal sengaja tidak dipakai — pemanggil yang benar selalu memeriksa err lebih dulu.

Presisi float64 di Lapangan #

Sebelum melihat demo, kita perlu mengakui satu sifat float64: angka desimal disimpan dalam biner, dan sebagian desimal tidak punya representasi biner yang persis. Contohnya terlihat langsung dari program kita:

bash
go run . tambah 0.1 0.2

Output yang Dihasilkan:

text
Hasil: 0.30000000000000004

Hasil matematisnya 0.3, tetapi 0.1 dan 0.2 masing-masing sudah disimpan sedikit meleset, sehingga jumlahnya pun meleset. Format %g yang dipakai main.go mencetak digit sependek yang masih mewakili nilai tersimpan secara unik — itulah digit panjang tersebut muncul.

Menjalankan Operasi #

Program utuh sudah tersedia pada bagian Berkas Sumber di bawah halaman ini — pemanggilan fungsi-fungsi di atas dari command line dibedah tuntas pada Bagian 3. Untuk sekarang, kita cukup membuktikan operasinya bekerja:

bash
go run . tambah 2 3
go run . bagi 10 4
go run . kali 6 7

Output yang Dihasilkan:

text
Hasil: 5
Hasil: 2.5
Hasil: 42

Ringkasan Bagian 2 #

Empat operasi aritmatika kini terimplementasi: tiga di antaranya fungsi murni satu baris, dan Bagi mengembalikan sentinel error ErrPembagianNol untuk penyebut nol. Mesin hitung selesai — yang belum ada adalah penjaga pintunya. Bagian 3 menyambungkan input pengguna mentah ke mesin ini lewat ParseInput beserta kontrak exit code.

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