21. Fungsi dan error

Kamu sudah memanggil fungsi sejak Bab 07: fmt.Println, strings.ToUpper, strconv.Atoi, rand.Intn. Sekarang kamu menulis fungsimu sendiri. Fungsi memberi nama pada sepotong pekerjaan yang bisa dipakai ulang, yaitu langkah bernama dari Bab 05 yang diubah menjadi kode.

Bab ini juga menepati janji dari Bagian 3. Bab ini menjelaskan nilai kedua dari strconv.Atoi dan ReadString, yang selama ini kamu buang dengan _.

Mendefinisikan dan memanggil

Definisi paling sederhana:

func greet() {
    fmt.Println("Hello!")
}

func main() {
    greet() // Hello!
    greet() // Hello!
}

Baca seperti ini: definisikan fungsi greet yang tidak menerima apa-apa dan menjalankan isinya saat dipanggil. func memulai definisi, () menampung apa yang diterimanya, dan isinya berada di antara { dan }. Panggil fungsi dengan menulis namanya dan ().

Sekarang kamu bisa melihat apa sebenarnya kerangka dari Bab 07. func main() { ... } adalah definisi fungsi biasa, dan main adalah fungsi yang dipanggil Go untukmu saat program mulai. Fungsi lainnya hanya berjalan kalau ada yang memanggilnya.

Mulai sekarang, blok kode yang mendefinisikan fungsi juga menampilkan main, tetapi tetap tidak menampilkan baris package dan import.

Fungsi ditulis di luar main, di tingkat paling atas file, dan urutannya tidak penting: main bisa memanggil fungsi yang didefinisikan lebih bawah. Fungsi di dalam main tidak bisa dikompilasi:

func main() {
    func greet() {
        fmt.Println("Hello!")
    }

    greet()
}
syntax error: unexpected greet, expected (
syntax error: non-declaration statement outside function body

Baris kedua hanya akibat dari baris pertama. Pindahkan greet keluar dari main, dan keduanya hilang.

Parameter

Fungsi menerima input lewat parameter di dalam tanda kurung. Setiap parameter punya nama dan tipe:

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    greet("Keiko") // Hello, Keiko!
    greet("Arif")  // Hello, Arif!
}

name hidup selama satu panggilan, dan pemanggil memberikan nilainya, yang disebut argumen. Kalau parameternya lebih dari satu, pisahkan dengan koma:

func add(a int, b int) {
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

func main() {
    add(3, 4) // 3 + 4 = 7
}

Kalau parameter yang berdampingan punya tipe yang sama, Go mengizinkan kamu menulis tipenya sekali saja. Kamu akan melihat ini di kode orang lain, dan artinya sama:

func add(a, b int) {

Tipe diperiksa sebelum program berjalan. Memberikan tipe yang salah berhenti dengan error seperti di Bab 11:

func add(a int, b int) {
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
}

func main() {
    add(3, "four")
}
cannot use "four" (untyped string constant) as int value in argument to add

Setiap argumen wajib ada

Go tidak punya parameter opsional, dan parameter tidak punya nilai bawaan. Panggilan harus memberikan persis argumen yang diminta fungsi. Terlalu sedikit:

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    greet()
}
not enough arguments in call to greet
    have ()
    want (string)

Terlalu banyak juga berhenti dengan cara yang sama. Pesannya berbunyi too many arguments, dengan baris have dan want yang sama untuk dibandingkan:

too many arguments in call to greet
    have (string, string)
    want (string)

Kalau kamu ingin nilai cadangan, tulis sendiri. Parameter adalah variabel di dalam fungsi, jadi pemeriksaan dari Bab 17 bisa dipakai padanya. Fungsi kedua dengan nama yang jelas lalu bisa memberikan nilainya:

func greet(name string) {
    if name == "" {
        name = "World"
    }
    fmt.Printf("Hello, %s!\n", name)
}

func greetEveryone() {
    greet("")
}

func main() {
    greet("Keiko")  // Hello, Keiko!
    greetEveryone() // Hello, World!
}

return: mengembalikan nilai

Untuk menghasilkan nilai alih-alih mencetak, pakai return, dan tulis tipe nilainya setelah tanda kurung:

func square(n int) int {
    return n * n
}

func main() {
    result := square(5)
    fmt.Println(result)        // 25
    fmt.Println(square(7))     // 49
    fmt.Println(square(3) + 1) // 10
}

return langsung mengakhiri fungsi dan menyerahkan nilainya kepada pemanggil. Fungsi yang menjanjikan nilai harus mencapai return di setiap jalur. Kalau tidak:

func isPositive(n int) bool {
    if n > 0 {
        return true
    }
}

func main() {
    fmt.Println(isPositive(5))
}
missing return

Go melaporkannya di } penutup, karena di situlah fungsi bisa berakhir tanpa nilai.

Fungsi yang tidak punya tipe setelah tanda kurungnya tidak mengembalikan apa-apa, dan Go tidak mengizinkan kamu memakai hasilnya sebagai nilai:

func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

func main() {
    result := greet("Keiko")
    fmt.Println(result)
}
greet("Keiko") (no value) used as value

Buka exercises/21/01-square.go. Tambahkan fungsi cube yang mengembalikan n * n * n, lalu cetak cube(3).

Beberapa nilai kembalian

Fungsi Go bisa mengembalikan beberapa nilai sekaligus. Tulis tipenya di dalam tanda kurung, dan pisahkan nilainya dengan koma di return:

func divide(a int, b int) (int, int) {
    return a / b, a % b
}

func main() {
    quotient, remainder := divide(17, 5)
    fmt.Println(quotient, remainder) // 3 2
}

Kamu sudah menjadi pihak penerima sejak Bab 14, ketika strconv.Atoi memberimu dua nilai. Ini penugasan dua variabel yang sama dengan yang kamu temui di Bab 11. Kamu harus menerima semuanya, karena satu variabel tidak bisa menampung dua nilai:

func divide(a int, b int) (int, int) {
    return a / b, a % b
}

func main() {
    result := divide(17, 5)
    fmt.Println(result)
}
assignment mismatch: 1 variable but divide returns 2 values

Kalau kamu hanya butuh sebagian, tulis _ di tempat yang lain. Hanya itulah yang selama ini dilakukan _ di text, _ :=:

func divide(a int, b int) (int, int) {
    return a / b, a % b
}

func main() {
    _, remainder := divide(20, 6)
    fmt.Println(remainder) // 2
}

Ini adalah exercises/21/02-divmod.go.

return awal untuk pemeriksaan penjaga

Karena return langsung mengakhiri fungsi, ini berguna untuk menangani kasus mudah lebih dulu:

func absolute(n int) int {
    if n >= 0 {
        return n
    }
    return -n
}

func main() {
    fmt.Println(absolute(5))  // 5
    fmt.Println(absolute(-7)) // 7
}

else tidak diperlukan. Kalau cabang pertama cocok, fungsi selesai di situ, dan baris setelah if tidak pernah berjalan. Ini disebut penjaga (guard), atau return awal.

Cakupan: tempat variabel hidup

Variabel yang dibuat di dalam fungsi hanya hidup selama satu panggilan. Begitu fungsi selesai, variabel itu hilang:

func counterStep() int {
    count := 0
    count = count + 1
    return count
}

func main() {
    fmt.Println(counterStep()) // 1
    fmt.Println(counterStep()) // 1
    fmt.Println(counterStep()) // 1
}

Setiap panggilan mendapat count yang baru. Parameter juga begitu: mengubahnya di dalam fungsi tidak mengubah variabel milik pemanggil. Fungsi lain juga tidak bisa melihatnya:

func setUp() {
    count := 0
    count = count + 1
}

func main() {
    setUp()
    fmt.Println(count)
}
undefined: count

Untuk menyimpan nilai antarpanggilan, deklarasikan variabelnya di luar semua fungsi. Semua fungsi dalam file itu lalu bisa memakainya:

var steps = 0

func step() int {
    steps = steps + 1
    return steps
}

func main() {
    fmt.Println(step()) // 1
    fmt.Println(step()) // 2
    fmt.Println(step()) // 3
}

Bab 11 mengatakan bahwa := hanya bisa dipakai di dalam fungsi, dan di sinilah hal itu berpengaruh. Di luar fungsi, kamu harus menulis var. exercises/21/04-outside-function.go sengaja memuat kesalahan ini, dan menjalankannya berhenti dengan:

# command-line-arguments
exercises\21\04-outside-function.go:11:1: syntax error: non-declaration statement outside function body

Pakai variabel di luar fungsi kalau memang perlu. Kebanyakan data lebih baik diberikan lewat parameter dan dikembalikan dengan return, karena dari panggilannya kamu bisa melihat apa yang dibaca dan diubah fungsi. Bab 26 menunjukkan cara yang lebih rapi untuk menyatukan data yang berkaitan. Contoh-contoh ini ada di exercises/21/03-counter-scope.go.

Error: nilai kedua

strconv.Atoi mengembalikan dua nilai: angkanya, dan laporan apakah konversinya berhasil. Laporan itu bertipe error. Isinya salah satu dari dua hal: nil, kata Go yang berarti "tidak ada apa-apa", kalau semuanya baik, atau keterangan tentang masalahnya. Simpan di variabel, biasanya bernama err, dan periksa tepat di baris berikutnya:

number, err := strconv.Atoi(text)
if err != nil {
    fmt.Println("That is not a whole number.")
    return
}

Baca err != nil sebagai "ada error". Ini adalah exercises/21/05-check-atoi.go, di dalam main:

reader := bufio.NewReader(os.Stdin)

fmt.Print("Enter a whole number: ")
text, _ := reader.ReadString('\n')

number, err := strconv.Atoi(strings.TrimSpace(text))
if err != nil {
    fmt.Println("That is not a whole number.")
    fmt.Println("The error says:", err)
    return
}

fmt.Println("You typed the number", number)

return di dalam main mengakhiri program. Tiga kali jalan, dengan mengetik 0, lalu abc, lalu hanya Enter:

Enter a whole number: 0
You typed the number 0

Enter a whole number: abc
That is not a whole number.
The error says: strconv.Atoi: parsing "abc": invalid syntax

Enter a whole number:
That is not a whole number.
The error says: strconv.Atoi: parsing "": invalid syntax

Ini menyelesaikan masalah dari Bab 14 dan 17: 0 sungguhan dan teks yang salah tidak lagi terlihat sama, karena kamu bertanya kepada err, bukan kepada angkanya. Kalau err bukan nil, angkanya bukan jawaban, jadi jangan dipakai.

strconv.ParseFloat bekerja dengan cara yang sama:

x, err := strconv.ParseFloat("abc", 64)
fmt.Println(x)   // 0
fmt.Println(err) // strconv.ParseFloat: parsing "abc": invalid syntax

err juga menangkap pengecualian yang disebut Bab 15, yaitu bilangan bulat yang terlalu besar untuk int:

n, err := strconv.Atoi("99999999999999999999")
fmt.Println(n)   // 9223372036854775807
fmt.Println(err) // strconv.Atoi: parsing "99999999999999999999": value out of range

Go tidak membiarkan kamu melupakan err secara tidak sengaja. Variabel yang tidak pernah dipakai membuat program gagal dikompilasi, seperti yang ditunjukkan Bab 10, jadi kamu harus memeriksa err atau membuangnya dengan sengaja memakai _:

number, err := strconv.Atoi("42")
fmt.Println(number)
err declared and not used

Mengembalikan error buatanmu sendiri

Fungsi buatanmu bisa melaporkan masalah dengan cara yang sama. Tambahkan error sebagai tipe kembalian terakhir. Saat gagal, kembalikan error yang dibuat dengan errors.New, dari package errors. Saat berhasil, kembalikan nil:

func safeDivide(a int, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

Kalau nilai harus ada di depan, 0 mengisi tempatnya saat gagal. Untuk memasukkan nilai ke dalam pesan, pakai fmt.Errorf, yang menerima verb yang sama dengan fmt.Sprintf. Fungsi yang hanya perlu melaporkan masalah cukup mengembalikan error:

func checkLevel(level int) error {
    if level < 1 || level > 99 {
        return fmt.Errorf("level %d is not from 1 to 99", level)
    }
    return nil
}

Pemanggil langsung memeriksanya, sama seperti pada strconv.Atoi. exercises/21/06-your-own-errors.go memanggil keduanya, dan mencetak:

Result: 3
Error: cannot divide by zero
<nil>
level 150 is not from 1 to 99

Dua baris terakhir menunjukkan dua hal yang bisa ditampung error. nil tercetak sebagai <nil>.

Buka exercises/21/06-your-own-errors.go. Tambahkan fungsi checkAge yang mengembalikan error kalau argumennya di bawah 0, dan nil kalau tidak. Panggil dari main dengan kedua jenis nilai.

Mengetahui bahwa input sudah habis

Bab 20 memperingatkan bahwa perulangan validasi mencetak pesannya selamanya kalau input habis. Ini terjadi kalau input berasal dari file, bukan dari keyboard. Di keyboard, pada Windows, menekan Ctrl + Z lalu Enter mengakhiri input dengan cara yang sama.

Nilai kedua dari ReadString melaporkan hal ini. Nilainya adalah error yang bukan nil kalau tidak ada lagi yang bisa dibaca:

text, err := reader.ReadString('\n')
line := strings.TrimSpace(text)
if err != nil && line == "" {
    fmt.Println("No more input.")
    break
}

Pemeriksaannya punya dua bagian karena baris terakhir sebuah file mungkin tidak punya akhiran baris. ReadString lalu memberimu teks itu dan error sekaligus, dan teksnya masih bagus. Hanya error tanpa teks yang berarti input sudah habis.

Ini adalah exercises/21/07-read-until-end.go, di dalam main:

reader := bufio.NewReader(os.Stdin)

total := 0
for {
    fmt.Print("Number: ")
    text, err := reader.ReadString('\n')
    line := strings.TrimSpace(text)
    if err != nil && line == "" {
        fmt.Println("No more input.")
        break
    }

    number, err := strconv.Atoi(line)
    if err != nil {
        fmt.Println("Skipped:", line)
        continue
    }
    total += number
}

fmt.Println("Total:", total)

Contoh sesi, dengan input yang habis setelah 7:

Number: 5
Number: abc
Skipped: abc
Number: 7
Number: No more input.
Total: 12

Perhatikan number, err := strconv.Atoi(line). err sudah ada di putaran ini, tetapi number baru, jadi := boleh dipakai, dan err dipakai ulang. Aturannya sama dengan di Bab 15.

Sekarang perulangan validasi bisa diselesaikan. Bab 20 mengatakan bahwa pemeriksaan rentangnya tidak bisa menerima 0 sebagai jawaban yang baik. Dengan err, bisa, dan perulangan berhenti kalau input habis. Ini adalah exercises/21/09-validate-volume.go, di dalam main:

reader := bufio.NewReader(os.Stdin)

var volume int
for {
    fmt.Print("Volume (0 to 10): ")
    text, err := reader.ReadString('\n')
    line := strings.TrimSpace(text)
    if err != nil && line == "" {
        fmt.Println("No more input.")
        return
    }

    volume, err = strconv.Atoi(line)
    if err == nil && volume >= 0 && volume <= 10 {
        break
    }
    fmt.Println("That is not a volume from 0 to 10.")
}

fmt.Println("Volume set to", volume)

Contoh sesi:

Volume (0 to 10): abc
That is not a volume from 0 to 10.
Volume (0 to 10): 150
That is not a volume from 0 to 10.
Volume (0 to 10): 0
Volume set to 0

Memasukkan baris pembacaan ke dalam fungsi

Bab 15 mengulang baris pembacaan untuk setiap nilai, dan menjanjikan cara yang lebih baik. Inilah caranya, di exercises/21/08-read-functions.go:

// Chapter 21 example: the input-reading lines, in functions of their own.
// Run with: go run exercises/21/08-read-functions.go

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

var reader = bufio.NewReader(os.Stdin)

// readLine prints the prompt and gives back one trimmed line. The error is
// not nil when there is no more input.
func readLine(prompt string) (string, error) {
    fmt.Print(prompt)
    text, err := reader.ReadString('\n')
    line := strings.TrimSpace(text)
    if err != nil && line == "" {
        return "", err
    }
    return line, nil
}

// readInt asks until it gets a whole number. The error is not nil when
// there is no more input.
func readInt(prompt string) (int, error) {
    for {
        line, err := readLine(prompt)
        if err != nil {
            return 0, err
        }

        number, err := strconv.Atoi(line)
        if err == nil {
            return number, nil
        }
        fmt.Println("That is not a whole number.")
    }
}

func main() {
    a, err := readInt("First number:  ")
    if err != nil {
        fmt.Println("No more input.")
        return
    }

    b, err := readInt("Second number: ")
    if err != nil {
        fmt.Println("No more input.")
        return
    }

    fmt.Printf("Sum: %d\n", a+b)
}

Beberapa hal dari bab ini bertemu di sini:

  • reader pindah ke luar main, memakai var, karena readLine juga membutuhkannya. Sebuah program tetap hanya perlu membuat satu.
  • readLine mengerjakan prompt, pembacaan, pemangkasan, dan pemeriksaan. Fungsi ini mengembalikan baris dan error.
  • readInt memanggil readLine, dan bertanya lagi sampai mendapat bilangan bulat. Kalau err bukan nil, error itu diteruskan kepada pemanggilnya.
  • main sekarang hanya menyebutkan apa yang diinginkannya, dan memeriksa apakah input sudah habis.

Contoh sesi, dan satu kali jalan tanpa input sama sekali:

First number:  3
Second number: abc
That is not a whole number.
Second number: 4
Sum: 7

First number:  No more input.

Mini-proyek Bagian 1 menunjukkan fungsi yang bukan bagian dari Go:

guess := readGuess()

Sekarang kamu bisa menulisnya. Isinya satu baris, karena return bisa meneruskan semua nilai dari panggilan lain:

func readGuess() (int, error) {
    return readInt("Guess: ")
}

Karena pembacaan bisa gagal, panggilan yang sebenarnya menerima dua nilai, dan memeriksa errornya:

guess, err := readGuess()
if err != nil {
    fmt.Println("No more input. The number was", target)
    return
}

exercises/21/10-read-guess.go adalah seluruh Game Tebak Angka yang ditulis dengan cara ini. Berbeda dari versi di Bagian 4, abc tidak lagi dihitung sebagai tebakan.

Buka exercises/21/08-read-functions.go. Tambahkan fungsi readFloat yang bekerja seperti readInt tetapi memakai strconv.ParseFloat, dan pakai untuk menanyakan harga.

PR

Starter PR ada di exercises/21/homework/. Setiap starter bisa langsung dijalankan dan mencetak satu baris pengganti. Tulis fungsimu di luar main.

Soal 1 — Sapa

Buka exercises/21/homework/01-greet.go. Tulis fungsi greet yang menerima name dan mencetak Hello, <name>!. Panggil dengan tiga nama yang berbeda.

Soal 2 — isEven

Buka exercises/21/homework/02-is-even.go. Tulis isEven, yang menerima int dan mengembalikan (bukan mencetak) true atau false untuk menyatakan apakah angka itu genap. Panggil pada beberapa nilai di dalam fmt.Println supaya nilai bool-nya terlihat.

Soal 3 — Clamp

Buka exercises/21/homework/03-clamp.go. Tulis clamp(x int, lo int, hi int) int, yang mengembalikan lo kalau x di bawah lo, hi kalau di atas hi, dan x kalau tidak keduanya. Uji dengan tiga panggilan yang mencakup setiap kasus.

Tantangan — Persen

Buka exercises/21/homework/04-percent.go. Tulis parsePercent(text string) (int, error). Fungsi ini mengubah text menjadi int dan mengembalikan angka itu dengan error nil. Kalau teksnya bukan angka, fungsi ini mengembalikan error dari strconv.Atoi. Kalau angkanya di bawah 0 atau di atas 100, fungsi ini mengembalikan error buatanmu sendiri dari errors.New. Tulis fungsi kedua, report, yang memanggilnya, langsung memeriksa error-nya, dan mencetak hasilnya. Panggil report dengan "85", "abc", dan "150".

Stuck atau sudah selesai? Buka halaman solusi PR.