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:
// 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:
- Baris 14 (
func Tambah(a, b float64) float64): tanda tangan fungsi menyatakan kontraknya — dua parameterfloat64masuk, satufloat64keluar. Tidak adaerrorpada nilai balik karena operasi ini tidak memiliki kondisi gagal. - 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. - Baris 18-21 dan 23-26 (
KurangdanKali): 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:
// 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:
// 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:
- Baris 30 (
func Bagi(a, b float64) (float64, error)): dua nilai balik — hasil perhitungan dan kemungkinanerror— adalah pola standar Go untuk operasi yang bisa gagal. - Baris 31-33 (
if b == 0 ...): pemeriksaan penyebut nol berdiri paling depan. Bila kondisi terpenuhi, fungsi mengembalikan0sebagai hasil pengganti danErrPembagianNolsebagai penanda kegagalan. - Baris 34 (
return a / b, nil): jalur normal mengembalikan hasil pembagian dengannilsebagai error. Angka0yang dikembalikan pada jalur gagal sengaja tidak dipakai — pemanggil yang benar selalu memeriksaerrlebih 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:
go run . tambah 0.1 0.2Output yang Dihasilkan:
Hasil: 0.30000000000000004Hasil 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:
go run . tambah 2 3
go run . bagi 10 4
go run . kali 6 7Output yang Dihasilkan:
Hasil: 5
Hasil: 2.5
Hasil: 42Ringkasan 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 #
- Operator aritmatika pada spesifikasi Go
— aturan resmi perilaku
+,-,*,/. - Dokumentasi errors.Is — cara resmi memeriksa sentinel error.
- Error handling and Go
— artikel resmi pola nilai balik
error.
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)
}
}