25. Package dan modul

Semua program yang kamu tulis sejauh ini berada dalam satu file. Program sungguhan tumbuh melewati itu. Go memecah program ke beberapa file memakai package: package adalah folder berisi file .go yang saling berkaitan, dan bisa menyediakan fungsi dan variabel untuk dipakai package lain.

Bab ini juga menjawab dua pertanyaan lama: apa arti sebenarnya dari package main, dan mengapa huruf kapital di awal punya arti khusus.

Satu folder adalah satu package

Setiap file .go dimulai dengan baris package, dan semua file dalam satu folder harus punya baris yang sama. File-file itu lalu bertindak sebagai satu package: fungsi di satu file bisa memanggil fungsi di file lain, tanpa import di antaranya.

Ini adalah exercises/25/examples/01-one-package/. Isinya tiga file. greet.go:

// Chapter 25 example: the second file of package main.

package main

import "fmt"

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

func bye(name string) {
    fmt.Printf("Goodbye, %s.\n", name)
}

main.go:

// Chapter 25 example: one package, split across two files.
//
// Run from this folder:
//   cd exercises/25/examples/01-one-package
//   go run .

package main

func main() {
    hello("Keiko")
    bye("Keiko")
}

File ketiga, go.mod, dijelaskan di bawah. Ada tiga hal yang perlu diperhatikan:

  • main.go memanggil hello tanpa import. Keduanya ada di package yang sama.
  • main.go tidak punya import "fmt", karena tidak memakai fmt. Import berlaku untuk setiap file, bukan untuk package.
  • greet.go tidak mengerjakan apa-apa sendiri. File ini hanya mendefinisikan fungsi.

Sampai sekarang kamu menjalankan satu file setiap kali. Untuk menjalankan seluruh package, masuk ke foldernya dan pakai go run ., dengan titik yang berarti "package di folder ini". Dari folder go-work-mu:

cd exercises/25/examples/01-one-package
go run .

Output:

Hello, Keiko!
Goodbye, Keiko.

Aturan dari Bab 06 untuk menjalankan semuanya dari go-work tidak berlaku di sini. Untuk kembali ke go-work sesudahnya, folder ini berada empat tingkat di bawah:

cd ../../../..

go run main.go hanya mengompilasi file yang kamu sebutkan, jadi tidak bisa melihat greet.go. Di dalam folder itu, perintahnya berhenti dengan:

# command-line-arguments
.\main.go:10:2: undefined: hello
.\main.go:11:2: undefined: bye

Fungsinya ada, tetapi Go tidak diberi tahu tentang filenya. Pakai go run ..

Arti sebenarnya dari package main

Bab 07 mengatakan bahwa package main menandai file sebagai program yang bisa dijalankan. Sekarang sisa kalimat itu masuk akal. Go punya dua jenis package:

  • package main membuat program. Package ini harus punya func main(), dan dari situlah program mulai.

  • Nama package lainnya membuat pustaka: kumpulan kode untuk di-import package lain. fmt dan strings adalah pustaka. Pustaka tidak punya func main, dan kamu tidak bisa menjalankannya. Kalau dicoba:

    package twopackages/greet is not a main package

Jadi kerangka yang selama ini kamu salin berkata: package ini adalah program, dan program mulai di sini.

Memakai package dengan import

Untuk membuat pustaka sendiri, taruh di foldernya sendiri, dengan nama package sendiri. Ini adalah exercises/25/examples/02-two-packages/:

02-two-packages/
    go.mod
    main.go
    greet/
        greet.go

greet/greet.go:

// Chapter 25 example: package greet, in its own folder.

package greet

import "fmt"

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

func Bye(name string) {
    fmt.Printf("Goodbye, %s.\n", name)
}

main.go:

// Chapter 25 example: a program that imports a package of your own.
//
// Run from this folder:
//   cd exercises/25/examples/02-two-packages
//   go run .

package main

import "twopackages/greet"

func main() {
    greet.Hello("Keiko")
    greet.Bye("Keiko")
}

Jalankan dari foldernya:

cd exercises/25/examples/02-two-packages
go run .

Output:

Hello, Keiko!
Goodbye, Keiko.

import "twopackages/greet" melakukan dua hal:

  1. Mencari package-nya. twopackages adalah nama modul ini, yang ditetapkan oleh go.mod, dan greet adalah folder di dalamnya.
  2. Memberimu nama greet, sehingga kamu bisa menulis greet.Hello, sama seperti import "fmt" memberimu fmt.Println.

Nama yang dipakai berasal dari baris package, yang menurut kebiasaan sama dengan nama foldernya. Itu juga sebabnya import "math/rand" memberimu rand.Intn: path-nya punya dua bagian, dan package-nya bernama rand.

Sebuah package dikompilasi sekali, berapa pun banyaknya file yang meng-import-nya. Kalau dua file sama-sama meng-import greet, keduanya berbagi package yang sama, termasuk variabel apa pun di dalamnya. Meng-import tidak menjalankan fungsi package itu. Hanya memanggilnya yang menjalankan.

Import yang tidak dipakai menghentikan program, seperti yang ditunjukkan Bab 10 untuk fmt:

# twopackages
.\main.go:9:8: "twopackages/greet" imported and not used

Mengapa huruf kapital punya arti khusus

Bab 05 dan 11 memintamu memulai nama dengan huruf kecil, dan menjanjikan penjelasannya. Inilah penjelasannya. Nama yang dimulai dengan huruf kapital diekspor: package lain bisa memakainya. Nama yang dimulai dengan huruf kecil bersifat pribadi bagi package-nya.

Itu sebabnya kamu selalu menulis fmt.Println dengan P kapital, dan mengapa greet.go di atas memakai Hello, bukan hello. Itu juga sebabnya kedua fungsi di 01-one-package boleh memakai huruf kecil: keduanya hanya dipakai di dalam package-nya sendiri.

Ini aturan bahasa, bukan sekadar kebiasaan. Kalau main.go memanggil greet.hello, Go berhenti:

# twopackages
.\main.go:12:8: undefined: greet.hello

Di dalam package main, tidak ada yang meng-import namamu, jadi huruf kecil adalah pilihan yang tepat. Itu sebabnya buku ini memakainya sejak awal. Huruf pertama juga berlaku untuk variabel dan konstanta di tingkat atas package, seperti yang akan kamu lihat di mini-proyek.

Nama pendek dengan alias import

Tidak ada cara untuk meng-import satu fungsi saja dari sebuah package. Setiap pemakaian punya nama package di depannya, dan itu memberi tahu pembaca asalnya. Tetapi kamu bisa memberi package nama yang lebih pendek untuk satu file, dengan menuliskannya sebelum path:

import (
    "fmt"

    mh "mathmodule/mathhelpers"
)

func main() {
    fmt.Println(mh.Double(7))
    fmt.Println(mh.Triple(7))
}

Untuk package kecil buatanmu, alias bersifat opsional. Nama package itu pendek, huruf kecil, dan satu kata, tanpa _. Jadi package berisi pembantu matematika bernama mathhelpers, bukan math_helpers.

Modul dan go.mod

Modul adalah folder berisi package dengan file go.mod di puncaknya. File ini memberi nama modul, dan nama itu menjadi awal setiap path import di dalamnya. Ini adalah exercises/25/examples/02-two-packages/go.mod:

module twopackages

go 1.22

Baris terakhir menyatakan untuk versi Go mana kode ini ditulis.

Satu file yang dijalankan dengan go run <file> tidak butuh modul, seperti kata Bab 06, karena hanya meng-import pustaka standar. Begitu kamu meng-import package buatanmu sendiri, Go butuh go.mod untuk mengetahui arti path itu. Folder-folder di bab ini sudah punya. Untuk memulai programmu sendiri, buat folder, masuk ke dalamnya, dan jalankan go mod init dengan nama pilihanmu:

mkdir mygame
cd mygame
go mod init mygame

Perintah itu mencetak:

go: creating new go.mod: module mygame

go.mod yang ditulisnya memuat versi yang terpasang di komputermu pada baris terakhir. Kamu hanya melakukan ini sekali untuk setiap program.

Tempat menjalankan perintah itu penting:

  • go run . harus berjalan di folder yang berisi package main. Di folder yang tidak punya go.mod di atasnya, perintah ini berhenti dengan:

    go: go.mod file not found in current directory or any parent directory; see 'go help modules'
  • Menjalankan go run exercises/25/examples/02-two-packages/main.go dari go-work juga gagal. Go mencari go.mod di folder tempat kamu berada, tidak menemukannya, dan berhenti dengan error yang memuat package twopackages/greet is not in std. Jadi cd dulu.

  • go vet ./... memeriksa setiap package dalam modul, kalau kamu menjalankannya dari folder puncak modul. Perintah ini tidak mencetak apa-apa kalau tidak menemukan masalah.

Pembantu pribadi di dalam package

Fungsi yang tidak boleh dipanggil package lain cukup diberi nama huruf kecil. Ini package di exercises/25/examples/03-private-helper/mathhelpers/mathhelpers.go:

// Chapter 25 example: package mathhelpers.

package mathhelpers

func clamp(x int, lo int, hi int) int {
    if x < lo {
        return lo
    }
    if x > hi {
        return hi
    }
    return x
}

func Double(x int) int {
    return clamp(x*2, -100, 100)
}

Double untuk semua orang, dan clamp adalah pembantu khusus package ini. File dalam package yang sama bisa memanggilnya, dan package lain tidak bisa. Go menegakkan aturan ini: memanggil mathhelpers.clamp dari main.go menghentikan program:

# privatehelper
.\main.go:20:26: clamp not exported by package mathhelpers

Dengan menjaga pembantu tetap pribadi, kamu bisa mengubahnya nanti tanpa merusak package lain.

Masuk ke exercises/25/examples/03-private-helper dan jalankan go run ., yang mencetak 14 dan 100. Lalu hapus tanda komentar pada baris terakhir main.go dan jalankan lagi untuk melihat error di atas.

PR

PR Bagian 5 punya modul sendiri untuk setiap soal: go.mod, main.go yang sudah jadi, dan package dalam folder yang harus kamu lengkapi. Buka terminal, cd ke folder soalnya, lalu jalankan go run .. Setiap soal bisa langsung dijalankan, tetapi mencetak teks pengganti atau jawaban yang salah sampai kamu menyelesaikan package-nya. Jalankan juga go vet ./... di sana.

Soal 1 — Package greet

Folder: exercises/25/homework/01-greet-module/.

Selesaikan greet/greet.go supaya Hello(name) mencetak Hello, <name>! dan Bye(name) mencetak Goodbye, <name>.. File main.go sudah jadi dan sudah memanggil kedua fungsi itu.

Soal 2 — Pembantu matematika

Folder: exercises/25/homework/02-math-helpers/.

Selesaikan package di mathhelpers/mathhelpers.go:

  • Double(x) mengembalikan x * 2.
  • Triple(x) mengembalikan x * 3.

main.go meng-import-nya dengan alias mh, memanggil keduanya, dan mencetak hasilnya.

Soal 3 — Penghitung

Folder: exercises/25/homework/03-counter/.

Selesaikan package di counter/counter.go. Package ini punya tiga fungsi:

  • Increment() menambah penghitung sebesar 1.
  • Get() mengembalikan nilai saat ini.
  • Reset() mengembalikan penghitung ke 0.

Penghitungnya sendiri adalah variabel di bagian atas file, di luar fungsi. Nilainya harus bertahan antarpanggilan (ingat kembali bagian cakupan di Bab 21). Beri nama huruf kecil, supaya hanya ketiga fungsi itu yang bisa menyentuhnya.

Tantangan — Alat string

Folder: exercises/25/homework/04-string-utils/.

Selesaikan package di stringutils/stringutils.go dengan setidaknya tiga fungsi. Saran:

  • Shout(s) mengembalikan s dalam huruf besar dengan tambahan !.
  • Echo(s, n) mengembalikan s yang diulang n kali dengan spasi di antaranya.
  • ReverseWords(s) mengembalikan kata-kata s dalam urutan terbalik.

Yang terakhir cukup menantang: pecah string menjadi kata, lalu balik slice-nya. Kalau terlalu berat, ganti dengan fungsi lain buatanmu sendiri, dan ubah main.go agar sesuai.

Stuck atau sudah selesai? Buka halaman solusi PR.