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:
| Operasi | Bentuk matematis | Contoh perintah |
|---|---|---|
tambah | a + b | kalkulator tambah 2 3 |
kurang | a - b | kalkulator kurang 10 4 |
kali | a × b | kalkulator kali 6 7 |
bagi | a ÷ b | kalkulator 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:
module example.com/kalkulator-terminal
go 1.27Struktur direktori proyek:
kalkulator-terminal/
├── go.mod
├── main.go
├── calculator.go
├── calculator_test.go
└── README.mdSetiap 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 go.mod pada Go Modules Reference
— aturan resmi isi berkas
go.mod. - Tutorial: Create a module — langkah resmi memulai module baru.
- Dokumentasi package strconv
— referensi
ParseFloatyang dipakai mengubah teks menjadi angka.
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)
}
}