Effective Go (indonesia)


Effective Go
Pendahuluan
Go adalah sebuah bahasa baru. Meskipun meminjam ide dari bahasa-bahasa yang sudah ada, Go memiliki properti tak biasa yang membuat program Go yang efektif memiliki karakter yang berbeda dari program yang ditulis dalam bahasa kerabatnya. Terjemahan langsung dari program C++ atau Java ke Go kemungkinan tidak akan menghasilkan hasil yang memuaskan—program Java ditulis dalam Java, bukan Go. Di sisi lain, memikirkan masalah dari perspektif Go dapat menghasilkan program yang sukses namun sangat berbeda. Dengan kata lain, untuk menulis Go dengan baik, penting untuk memahami properti dan idiom-idiomnya. Penting juga untuk mengetahui konvensi yang sudah mapan untuk pemrograman di Go, seperti penamaan, format penulisan, konstruksi program, dan sebagainya, agar program yang Anda tulis akan mudah dipahami oleh programmer Go lainnya.
Dokumen ini memberikan tips untuk menulis kode Go yang jelas dan idiomatik. Dokumen ini melengkapi spesifikasi bahasa, Tour of Go, dan How to Write Go Code, yang semuanya harus Anda baca terlebih dahulu.
Catatan ditambahkan Januari 2022: Dokumen ini ditulis untuk rilis Go pada tahun 2009, dan belum diperbarui secara signifikan sejak saat itu. Meskipun ini adalah panduan yang baik untuk memahami cara menggunakan bahasa itu sendiri, berkat stabilitas bahasa, dokumen ini hanya sedikit membahas tentang pustaka (libraries) dan sama sekali tidak membahas perubahan signifikan pada ekosistem Go sejak ditulis, seperti sistem build, pengujian (testing), modul, dan polimorfisme. Tidak ada rencana untuk memperbaruinya, karena begitu banyak yang telah terjadi dan serangkaian dokumen, blog, dan buku yang besar dan terus berkembang telah melakukan pekerjaan yang baik dalam menjelaskan penggunaan Go modern. Effective Go terus bermanfaat, tetapi pembaca harus memahami bahwa ini jauh dari panduan yang lengkap. Lihat isu 28782 untuk konteks.
Contoh (Examples)
Source code package Go dimaksudkan untuk berfungsi tidak hanya sebagai pustaka inti tetapi juga sebagai contoh cara menggunakan bahasa tersebut. Selain itu, banyak dari package tersebut berisi contoh program yang dapat dieksekusi, mandiri, dan dapat Anda jalankan langsung dari situs web go.dev, seperti yang satu ini (jika perlu, klik pada kata "Example" untuk membukanya). Jika Anda memiliki pertanyaan tentang cara mendekati suatu masalah atau bagaimana sesuatu mungkin diimplementasikan, dokumentasi, kode, dan contoh di dalam pustaka dapat memberikan jawaban, ide, dan latar belakang.
Format Penulisan (Formatting)
Masalah format penulisan adalah yang paling sering diperdebatkan tetapi paling tidak penting. Orang dapat beradaptasi dengan gaya format yang berbeda tetapi lebih baik jika mereka tidak perlu melakukannya, dan lebih sedikit waktu yang dihabiskan untuk topik tersebut jika semua orang menganut gaya yang sama. Masalahnya adalah bagaimana mendekati Utopia ini tanpa panduan gaya preskriptif yang panjang.
Dengan Go, kami mengambil pendekatan yang tidak biasa dan membiarkan mesin menangani sebagian besar masalah format. Program gofmt
(juga tersedia sebagai go fmt
, yang beroperasi pada level package bukan level file source) membaca program Go dan menghasilkan source code dalam gaya indentasi dan perataan vertikal yang standar, dengan mempertahankan dan jika perlu memformat ulang komentar. Jika Anda ingin tahu bagaimana menangani beberapa situasi tata letak baru, jalankan gofmt
; jika jawabannya tidak tampak benar, atur ulang program Anda (atau ajukan bug tentang gofmt
), jangan mengakali itu.
Sebagai contoh, tidak perlu menghabiskan waktu merapikan komentar pada field sebuah struct. Gofmt
akan melakukannya untuk Anda. Diberikan deklarasi:
type T struct {
name string // name of the object
value int // its value
}
gofmt
akan merapikan kolom-kolomnya:
type T struct {
name string // name of the object
value int // its value
}
Semua kode Go dalam package standar telah diformat dengan gofmt
.
Beberapa detail format tetap ada. Secara sangat singkat:
Indentasi Kami menggunakan tab untuk indentasi dan
gofmt
menghasilkannya secara default. Gunakan spasi hanya jika Anda harus.Panjang Baris Go tidak memiliki batas panjang baris. Jangan khawatir tentang meluapnya kartu pons. Jika sebuah baris terasa terlalu panjang, bungkus baris itu dan beri indentasi dengan tab tambahan.
Tanda Kurung Go membutuhkan lebih sedikit tanda kurung daripada C dan Java: struktur kontrol (
if
,for
,switch
) tidak memiliki tanda kurung dalam sintaksnya. Selain itu, hierarki presedensi operator lebih pendek dan lebih jelas, jadix<<8 + y<<16
berarti seperti yang tersirat oleh spasinya, tidak seperti di bahasa lain.
Komentar (Commentary)
Go menyediakan komentar blok gaya C /* */
dan komentar baris gaya C++ //
. Komentar baris adalah yang umum digunakan; komentar blok sebagian besar muncul sebagai komentar paket, tetapi juga berguna di dalam sebuah ekspresi atau untuk menonaktifkan sebagian besar kode.
Komentar yang muncul sebelum deklarasi tingkat atas (top-level), tanpa ada baris baru yang menyela, dianggap sebagai dokumentasi untuk deklarasi itu sendiri. "Komentar dokumentasi" atau "doc comments" ini adalah dokumentasi utama untuk sebuah paket atau perintah Go. Untuk informasi lebih lanjut tentang komentar dokumentasi, lihat “Go Doc Comments”.
Penamaan (Names)
Penamaan sama pentingnya di Go seperti di bahasa lain. Nama bahkan memiliki efek semantik: visibilitas sebuah nama di luar sebuah paket ditentukan oleh apakah karakter pertamanya adalah huruf kapital. Oleh karena itu, ada baiknya meluangkan sedikit waktu untuk membahas konvensi penamaan dalam program Go.
Nama Paket (Package names)
Ketika sebuah paket diimpor, nama paket menjadi pengakses untuk kontennya. Setelah
import "bytes"
paket yang mengimpor dapat menggunakan bytes.Buffer
. Akan sangat membantu jika semua orang yang menggunakan paket dapat menggunakan nama yang sama untuk merujuk pada kontennya, yang menyiratkan bahwa nama paket haruslah baik: pendek, ringkas, dan menggugah. Berdasarkan konvensi, paket diberi nama dengan huruf kecil dan satu kata; seharusnya tidak perlu ada garis bawah (underscore) atau mixedCaps
. Pilihlah keringkasan, karena semua orang yang menggunakan paket Anda akan mengetik nama itu. Dan jangan khawatir tentang tubrukan nama a priori. Nama paket hanyalah nama default untuk impor; nama itu tidak harus unik di semua kode sumber, dan dalam kasus tubrukan yang jarang terjadi, paket yang mengimpor dapat memilih nama yang berbeda untuk digunakan secara lokal. Bagaimanapun, kebingungan jarang terjadi karena nama file dalam impor menentukan paket mana yang sedang digunakan.
Konvensi lain adalah bahwa nama paket adalah nama dasar dari direktori sumbernya; paket di src/encoding/base64
diimpor sebagai "encoding/base64"
tetapi memiliki nama base64
, bukan encoding_base64
dan bukan encodingBase64
.
Pengimpor paket akan menggunakan nama tersebut untuk merujuk pada kontennya, sehingga nama yang diekspor dalam paket dapat memanfaatkan fakta tersebut untuk menghindari pengulangan. (Jangan gunakan notasi import .
, yang dapat menyederhanakan pengujian yang harus berjalan di luar paket yang sedang diuji, tetapi sebaiknya dihindari.) Sebagai contoh, tipe pembaca buffer di paket bufio
disebut Reader
, bukan BufReader
, karena pengguna melihatnya sebagai bufio.Reader
, yang merupakan nama yang jelas dan ringkas. Selain itu, karena entitas yang diimpor selalu dialamatkan dengan nama paketnya, bufio.Reader
tidak berkonflik dengan io.Reader
. Demikian pula, fungsi untuk membuat instance baru dari ring.Ring
—yang merupakan definisi dari sebuah konstruktor di Go—biasanya akan disebut NewRing
, tetapi karena Ring
adalah satu-satunya tipe yang diekspor oleh paket, dan karena paket tersebut bernama ring
, maka cukup disebut New
, yang dilihat oleh klien paket sebagai ring.New
. Gunakan struktur paket untuk membantu Anda memilih nama yang baik.
Contoh singkat lainnya adalah once.Do
; once.Do
(setup)
terbaca dengan baik dan tidak akan menjadi lebih baik dengan menulis once.DoOrWaitUntilDone(setup)
. Nama yang panjang tidak secara otomatis membuat sesuatu menjadi lebih mudah dibaca. Komentar dokumentasi yang membantu seringkali bisa lebih berharga daripada nama yang terlalu panjang.
Getters
Go tidak menyediakan dukungan otomatis untuk getter dan setter. Tidak ada yang salah dengan menyediakan getter dan setter sendiri, dan seringkali pantas untuk melakukannya, tetapi bukanlah hal yang idiomatik maupun perlu untuk menempatkan kata Get
pada nama getter. Jika Anda memiliki field bernama owner
(huruf kecil, tidak diekspor), metode getter-nya harus bernama Owner
(huruf kapital, diekspor), bukan GetOwner
. Penggunaan nama dengan huruf kapital untuk ekspor menyediakan mekanisme untuk membedakan field dari metodenya. Fungsi setter, jika diperlukan, kemungkinan akan bernama SetOwner
. Kedua nama tersebut terbaca dengan baik dalam praktiknya:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
Nama Interface (Interface names)
Berdasarkan konvensi, interface dengan satu metode dinamai dengan nama metode ditambah akhiran -er atau modifikasi serupa untuk membentuk kata benda agen (agent noun): Reader
, Writer
, Formatter
, CloseNotifier
, dll.
Ada sejumlah nama seperti itu dan sangat produktif untuk menghormatinya beserta nama fungsi yang mereka representasikan. Read
, Write
, Close
, Flush
, String
dan sebagainya memiliki signature dan makna kanonis. Untuk menghindari kebingungan, jangan berikan salah satu dari nama-nama itu pada metode Anda kecuali jika ia memiliki signature dan makna yang sama. Sebaliknya, jika tipe Anda mengimplementasikan sebuah metode dengan makna yang sama seperti metode pada tipe yang sudah dikenal luas, berikan nama dan signature yang sama; sebut metode konverter-string Anda String
, bukan ToString
.
MixedCaps
Akhirnya, konvensi di Go adalah menggunakan MixedCaps
atau mixedCaps
daripada garis bawah (underscore) untuk menulis nama yang terdiri dari beberapa kata.
Titik Koma (Semicolons)
Seperti C, tata bahasa formal Go menggunakan titik koma untuk mengakhiri pernyataan, tetapi tidak seperti di C, titik koma tersebut tidak muncul di dalam source code. Sebaliknya, lexer (pemindai) menggunakan aturan sederhana untuk menyisipkan titik koma secara otomatis saat memindai, sehingga teks input sebagian besar bebas dari titik koma.
Aturannya adalah ini. Jika token terakhir sebelum baris baru adalah sebuah identifier (yang mencakup kata-kata seperti int
dan float64
), sebuah literal dasar seperti angka atau konstanta string, atau salah satu dari token berikut:
break continue fallthrough return ++ -- ) }
maka lexer selalu menyisipkan titik koma setelah token tersebut. Ini dapat diringkas sebagai, "jika baris baru datang setelah token yang dapat mengakhiri sebuah pernyataan, sisipkan titik koma".
Titik koma juga dapat dihilangkan tepat sebelum kurung kurawal penutup, sehingga pernyataan seperti:
go func() { for { dst <- <-src } }()
tidak memerlukan titik koma. Program Go yang idiomatik hanya memiliki titik koma di tempat-tempat seperti klausa perulangan for
, untuk memisahkan elemen inisialisasi, kondisi, dan kelanjutan. Titik koma juga diperlukan untuk memisahkan beberapa pernyataan dalam satu baris, jika Anda menulis kode dengan cara seperti itu.
Salah satu konsekuensi dari aturan penyisipan titik koma adalah Anda tidak dapat menempatkan kurung kurawal pembuka dari sebuah struktur kontrol (if
, for
, switch
, atau select
) di baris berikutnya. Jika Anda melakukannya, titik koma akan disisipkan sebelum kurung kurawal, yang dapat menyebabkan efek yang tidak diinginkan. Tulislah seperti ini:
if i < f() {
g()
}
bukan seperti ini:
if i < f() // salah!
{ // salah!
g()
}
Struktur Kontrol (Control structures)
Struktur kontrol Go terkait dengan C tetapi berbeda dalam hal-hal penting. Tidak ada perulangan do
atau while
, hanya ada for
yang sedikit lebih umum; switch
lebih fleksibel; if
dan switch
menerima pernyataan inisialisasi opsional seperti pada for
; pernyataan break
dan continue
dapat mengambil label opsional untuk mengidentifikasi apa yang harus dihentikan atau dilanjutkan; dan ada struktur kontrol baru termasuk type switch
dan pemultipleks komunikasi multi-arah, select
. Sintaksnya juga sedikit berbeda: tidak ada tanda kurung dan badan (body) harus selalu diapit kurung kurawal.
If
Di Go, sebuah if
sederhana terlihat seperti ini:
if x > 0 {
return y
}
Kurung kurawal yang wajib mendorong penulisan pernyataan if
sederhana dalam beberapa baris. Ini adalah gaya yang baik untuk melakukannya, terutama ketika badan pernyataan berisi pernyataan kontrol seperti return
atau break
.
Karena if
dan switch
menerima pernyataan inisialisasi, umum untuk melihatnya digunakan untuk menyiapkan variabel lokal.
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
Di pustaka Go, Anda akan menemukan bahwa ketika pernyataan if
tidak mengalir ke pernyataan berikutnya—yaitu, badannya berakhir dengan break
, continue
, goto
, atau return
—maka else
yang tidak perlu akan dihilangkan.
f, err := os.Open(name)
if err != nil {
return err
}
codeUsing(f)
Ini adalah contoh situasi umum di mana kode harus waspada terhadap serangkaian kondisi kesalahan. Kode tersebut mudah dibaca jika alur kontrol yang berhasil berjalan ke bawah halaman, menghilangkan kasus-kasus kesalahan saat muncul. Karena kasus kesalahan cenderung berakhir dengan pernyataan return
, kode yang dihasilkan tidak memerlukan pernyataan else
.
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.Close()
return err
}
codeUsing(f, d)
Deklarasi Ulang dan Penugasan Ulang (Redeclaration and reassignment)
Sedikit selingan: Contoh terakhir di bagian sebelumnya menunjukkan detail tentang cara kerja bentuk deklarasi singkat :=
. Deklarasi yang memanggil os.Open
berbunyi,
f, err := os.Open(name)
Pernyataan ini mendeklarasikan dua variabel, f
dan err
. Beberapa baris kemudian, panggilan ke f.Stat
berbunyi,
d, err := f.Stat()
yang terlihat seolah-olah mendeklarasikan d
dan err
. Namun, perhatikan bahwa err
muncul di kedua pernyataan. Duplikasi ini legal: err
dideklarasikan oleh pernyataan pertama, tetapi hanya ditugaskan ulang (re-assigned) pada pernyataan kedua. Ini berarti bahwa panggilan ke f.Stat
menggunakan variabel err
yang sudah ada yang dideklarasikan di atas, dan hanya memberinya nilai baru.
Dalam deklarasi :=
, sebuah variabel v
dapat muncul meskipun sudah dideklarasikan, asalkan:
deklarasi ini berada dalam cakupan (scope) yang sama dengan deklarasi
v
yang sudah ada (jikav
sudah dideklarasikan di cakupan luar, deklarasi akan membuat variabel baru §),nilai yang sesuai dalam inisialisasi dapat ditugaskan ke
v
, danada setidaknya satu variabel lain yang dibuat oleh deklarasi tersebut.
Properti yang tidak biasa ini murni pragmatisme, membuatnya mudah untuk menggunakan satu nilai err
, misalnya, dalam rantai if-else
yang panjang. Anda akan sering melihatnya digunakan.
§ Perlu dicatat di sini bahwa dalam Go, cakupan parameter fungsi dan nilai kembalian adalah sama dengan badan fungsi, meskipun secara leksikal mereka muncul di luar kurung kurawal yang melingkupi badan tersebut.
For
Perulangan for
di Go mirip—tapi tidak sama—dengan for
di C. Ia menyatukan for
dan while
dan tidak ada do-while
. Ada tiga bentuk, hanya satu yang memiliki titik koma.
// Seperti for di C
for init; condition; post { }
// Seperti while di C
for condition { }
// Seperti for(;;) di C
for { }
Deklarasi singkat memudahkan untuk mendeklarasikan variabel indeks langsung di dalam perulangan.
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
Jika Anda melakukan perulangan pada array, slice, string, atau map, atau membaca dari sebuah channel, klausa range
dapat mengelola perulangan tersebut.
for key, value := range oldMap {
newMap[key] = value
}
Jika Anda hanya memerlukan item pertama dalam rentang (key atau index), hilangkan yang kedua:
for key := range m {
if key.expired() {
delete(m, key)
}
}
Jika Anda hanya memerlukan item kedua dalam rentang (value), gunakan blank identifier, sebuah garis bawah, untuk membuang yang pertama:
sum := 0
for _, value := range array {
sum += value
}
Blank identifier memiliki banyak kegunaan, seperti yang dijelaskan di bagian selanjutnya.
Untuk string, range
melakukan lebih banyak pekerjaan untuk Anda, memecah setiap code point Unicode dengan mem-parsing UTF-8. Pengkodean yang salah akan mengonsumsi satu byte dan menghasilkan rune pengganti U+FFFD. (Nama rune
(dengan tipe bawaan terkait) adalah terminologi Go untuk satu code point Unicode. Lihat spesifikasi bahasa untuk detailnya.) Perulangan:
for pos, char := range "日本\x80語" { // \x80 adalah encoding UTF-8 yang ilegal
fmt.Printf("karakter %#U dimulai pada posisi byte %d\n", char, pos)
}
mencetak:
karakter U+65E5 '日' dimulai pada posisi byte 0
karakter U+672C '本' dimulai pada posisi byte 3
karakter U+FFFD '�' dimulai pada posisi byte 6
karakter U+8A9E '語' dimulai pada posisi byte 7
Akhirnya, Go tidak memiliki operator koma, dan ++
serta --
adalah pernyataan, bukan ekspresi. Jadi, jika Anda ingin menjalankan beberapa variabel dalam sebuah for
, Anda harus menggunakan penugasan paralel (meskipun itu menghalangi penggunaan ++
dan --
).
// Membalikkan sebuah slice a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
Switch
switch
di Go lebih umum daripada di C. Ekspresinya tidak harus konstan atau bahkan integer, kasus-kasusnya dievaluasi dari atas ke bawah hingga ditemukan kecocokan, dan jika switch
tidak memiliki ekspresi, ia akan beralih pada nilai true
. Oleh karena itu, mungkin—dan idiomatik—untuk menulis rantai if-else-if-else
sebagai sebuah switch
.
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
return 0
}
Tidak ada fall-through otomatis, tetapi kasus-kasus dapat disajikan dalam daftar yang dipisahkan koma.
func shouldEscape(c byte) bool {
switch c {
case ' ', '?', '&', '=', '#', '+', '%':
return true
}
return false
}
Meskipun tidak umum di Go seperti di beberapa bahasa mirip C lainnya, pernyataan break
dapat digunakan untuk menghentikan switch
lebih awal. Namun, terkadang perlu untuk keluar dari perulangan di sekitarnya, bukan dari switch
, dan di Go hal ini dapat dicapai dengan meletakkan label pada perulangan dan "breaking" ke label tersebut. Contoh ini menunjukkan kedua penggunaan tersebut.
Loop:
for n := 0; n < len(src); n += size {
switch {
case src[n] < sizeOne:
if validateOnly {
break
}
size = 1
update(src[n])
case src[n] < sizeTwo:
if n+1 >= len(src) {
err = errShortInput
break Loop
}
if validateOnly {
break
}
size = 2
update(src[n] + src[n+1]<<shift)
}
}
Tentu saja, pernyataan continue
juga menerima label opsional tetapi hanya berlaku untuk perulangan.
Untuk menutup bagian ini, berikut adalah rutin perbandingan untuk byte slices yang menggunakan dua pernyataan switch
:
// Compare mengembalikan integer yang membandingkan dua byte slice,
// secara leksikografis.
// Hasilnya akan 0 jika a == b, -1 jika a < b, dan +1 jika a > b
func Compare(a, b []byte) int {
for i := 0; i < len(a) && i < len(b); i++ {
switch {
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
switch {
case len(a) > len(b):
return 1
case len(a) < len(b):
return -1
}
return 0
}
Type switch
Sebuah switch
juga dapat digunakan untuk menemukan tipe dinamis dari variabel interface. type switch
seperti itu menggunakan sintaks dari type assertion dengan kata kunci type
di dalam tanda kurung. Jika switch
mendeklarasikan sebuah variabel dalam ekspresinya, variabel tersebut akan memiliki tipe yang sesuai di setiap klausa. Juga idiomatik untuk menggunakan kembali nama dalam kasus seperti itu, yang secara efektif mendeklarasikan variabel baru dengan nama yang sama tetapi tipe yang berbeda di setiap kasus.
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("tipe tak terduga %T\n", t) // %T mencetak tipe apa pun yang dimiliki t
case bool:
fmt.Printf("boolean %t\n", t) // t memiliki tipe bool
case int:
fmt.Printf("integer %d\n", t) // t memiliki tipe int
case *bool:
fmt.Printf("pointer ke boolean %t\n", *t) // t memiliki tipe *bool
case *int:
fmt.Printf("pointer ke integer %d\n", *t) // t memiliki tipe *int
}
Fungsi (Functions)
Nilai Kembalian Ganda (Multiple return values)
Salah satu fitur unik Go adalah fungsi dan metode dapat mengembalikan banyak nilai. Bentuk ini dapat digunakan untuk memperbaiki beberapa idiom kikuk dalam program C: pengembalian error in-band seperti -1
untuk EOF
dan memodifikasi argumen yang dilewatkan melalui alamat.
Di C, error penulisan ditandai dengan hitungan negatif dengan kode error disembunyikan di lokasi yang volatile. Di Go, Write
dapat mengembalikan hitungan dan sebuah error: "Ya, Anda menulis beberapa byte tetapi tidak semuanya karena perangkat sudah penuh". Tanda tangan metode Write
pada file dari paket os
adalah:
func (file *File) Write(b []byte) (n int, err error)
dan seperti yang dikatakan dokumentasi, ia mengembalikan jumlah byte yang ditulis dan error non-nil ketika n != len(b)
. Ini adalah gaya yang umum; lihat bagian tentang penanganan error untuk contoh lebih lanjut.
Pendekatan serupa meniadakan kebutuhan untuk meneruskan pointer ke nilai kembalian untuk mensimulasikan parameter referensi. Berikut adalah fungsi sederhana untuk mengambil angka dari posisi dalam byte slice, mengembalikan angka dan posisi berikutnya.
func nextInt(b []byte, i int) (int, int) {
for ; i < len(b) && !isDigit(b[i]); i++ {
}
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i]) - '0'
}
return x, i
}
Anda bisa menggunakannya untuk memindai angka-angka dalam slice input b
seperti ini:
for i := 0; i < len(b); {
x, i = nextInt(b, i)
fmt.Println(x)
}
Parameter Hasil Bernama (Named result parameters)
"Parameter" kembalian atau hasil dari sebuah fungsi Go dapat diberi nama dan digunakan sebagai variabel biasa, sama seperti parameter masuk. Ketika diberi nama, mereka diinisialisasi ke nilai nol untuk tipe mereka saat fungsi dimulai; jika fungsi mengeksekusi pernyataan return
tanpa argumen, nilai saat ini dari parameter hasil digunakan sebagai nilai yang dikembalikan.
Nama-nama ini tidak wajib tetapi dapat membuat kode lebih pendek dan lebih jelas: mereka adalah dokumentasi. Jika kita menamai hasil dari nextInt
, menjadi jelas int
yang dikembalikan adalah yang mana.
func nextInt(b []byte, pos int) (value, nextPos int) {
Karena hasil yang dinamai diinisialisasi dan terikat pada return
tanpa hiasan, mereka dapat menyederhanakan sekaligus memperjelas. Berikut adalah versi io.ReadFull
yang menggunakannya dengan baik:
func ReadFull(r Reader, buf []byte) (n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
Defer
Pernyataan defer
di Go menjadwalkan pemanggilan fungsi (fungsi yang ditunda) untuk dijalankan segera sebelum fungsi yang mengeksekusi defer
tersebut kembali. Ini adalah cara yang tidak biasa tetapi efektif untuk menangani situasi seperti sumber daya yang harus dilepaskan terlepas dari jalur mana yang diambil fungsi untuk kembali. Contoh kanonisnya adalah membuka kunci mutex atau menutup file.
// Contents mengembalikan isi file sebagai string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.Close() // f.Close akan berjalan setelah kita selesai.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append akan dibahas nanti.
if err != nil {
if err == io.EOF {
break
}
return "", err // f akan ditutup jika kita kembali di sini.
}
}
return string(result), nil // f akan ditutup jika kita kembali di sini.
}
Menunda panggilan ke fungsi seperti Close
memiliki dua keuntungan. Pertama, ini menjamin bahwa Anda tidak akan pernah lupa untuk menutup file, sebuah kesalahan yang mudah dibuat jika Anda kemudian mengedit fungsi untuk menambahkan jalur return
baru. Kedua, ini berarti close
berada di dekat open
, yang jauh lebih jelas daripada menempatkannya di akhir fungsi.
Argumen untuk fungsi yang ditunda (yang termasuk receiver jika fungsi tersebut adalah metode) dievaluasi ketika defer
dieksekusi, bukan ketika call
(panggilan) dieksekusi. Selain menghindari kekhawatiran tentang variabel yang nilainya berubah saat fungsi dieksekusi, ini berarti satu situs panggilan defer
dapat menunda beberapa eksekusi fungsi. Berikut adalah contoh konyol.
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
Fungsi yang ditunda dieksekusi dalam urutan LIFO (Last-In, First-Out), jadi kode ini akan menyebabkan 4 3 2 1 0
dicetak ketika fungsi kembali. Contoh yang lebih masuk akal adalah cara sederhana untuk melacak eksekusi fungsi melalui program. Kita bisa menulis beberapa rutin pelacakan sederhana seperti ini:
func trace(s string) { fmt.Println("memasuki:", s) }
func untrace(s string) { fmt.Println("meninggalkan:", s) }
// Gunakan seperti ini:
func a() {
trace("a")
defer untrace("a")
// lakukan sesuatu....
}
Kita bisa melakukan lebih baik dengan memanfaatkan fakta bahwa argumen untuk fungsi yang ditunda dievaluasi saat defer
dieksekusi. Rutin pelacakan dapat menyiapkan argumen untuk rutin pelepasan pelacakan. Contoh ini:
func trace(s string) string {
fmt.Println("memasuki:", s)
return s
}
func un(s string) {
fmt.Println("meninggalkan:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("di dalam a")
}
func b() {
defer un(trace("b"))
fmt.Println("di dalam b")
a()
}
func main() {
b()
}
mencetak:
memasuki: b
di dalam b
memasuki: a
di dalam a
meninggalkan: a
meninggalkan: b
Bagi pemrogram yang terbiasa dengan manajemen sumber daya tingkat blok dari bahasa lain, defer
mungkin tampak aneh, tetapi aplikasi yang paling menarik dan kuat justru datang dari fakta bahwa ia bukan berbasis blok tetapi berbasis fungsi. Di bagian tentang panic
dan recover
kita akan melihat contoh lain dari kemungkinannya.
Data
Alokasi dengan new
Go memiliki dua primitif alokasi, fungsi bawaan new
dan make
. Mereka melakukan hal yang berbeda dan berlaku untuk tipe yang berbeda, yang bisa membingungkan, tetapi aturannya sederhana. Mari kita bahas new
terlebih dahulu. Ini adalah fungsi bawaan yang mengalokasikan memori, tetapi tidak seperti yang bernama sama di beberapa bahasa lain, ia tidak menginisialisasi memori, ia hanya mengisinya dengan nol (zeros). Artinya, new(T)
mengalokasikan penyimpanan yang di-nol-kan untuk item baru bertipe T
dan mengembalikan alamatnya, sebuah nilai bertipe *T
. Dalam terminologi Go, ia mengembalikan pointer ke nilai nol yang baru dialokasikan dari tipe T
.
Karena memori yang dikembalikan oleh new
di-nol-kan, akan sangat membantu untuk mengatur saat merancang struktur data Anda bahwa nilai nol dari setiap tipe dapat digunakan tanpa inisialisasi lebih lanjut. Ini berarti pengguna struktur data dapat membuat satu dengan new
dan langsung bekerja. Misalnya, dokumentasi untuk bytes.Buffer
menyatakan bahwa "nilai nol untuk Buffer
adalah buffer kosong yang siap digunakan." Demikian pula, sync.Mutex
tidak memiliki konstruktor eksplisit atau metode Init
. Sebaliknya, nilai nol untuk sync.Mutex
didefinisikan sebagai mutex yang tidak terkunci.
Properti nilai-nol-bermanfaat ini bekerja secara transitif. Pertimbangkan deklarasi tipe ini.
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
Nilai dari tipe SyncedBuffer
juga siap digunakan segera setelah dialokasikan atau hanya dideklarasikan. Dalam cuplikan berikutnya, baik p
maupun v
akan bekerja dengan benar tanpa pengaturan lebih lanjut.
p := new(SyncedBuffer) // tipe *SyncedBuffer
var v SyncedBuffer // tipe SyncedBuffer
Konstruktor dan Literal Komposit (Constructors and composite literals)
Terkadang nilai nol tidak cukup baik dan konstruktor inisialisasi diperlukan, seperti dalam contoh yang berasal dari paket os
ini.
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
Ada banyak boilerplate di sana. Kita bisa menyederhanakannya menggunakan literal komposit, yaitu ekspresi yang membuat instance baru setiap kali dievaluasi.
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
Perhatikan bahwa, tidak seperti di C, sangat boleh untuk mengembalikan alamat variabel lokal; penyimpanan yang terkait dengan variabel tersebut akan bertahan setelah fungsi kembali. Faktanya, mengambil alamat dari literal komposit mengalokasikan instance baru setiap kali dievaluasi, jadi kita bisa menggabungkan dua baris terakhir ini.
return &File{fd, name, nil, 0}
Field dari literal komposit diletakkan berurutan dan semuanya harus ada. Namun, dengan melabeli elemen secara eksplisit sebagai pasangan field:value
, inisialisasi dapat muncul dalam urutan apa pun, dengan yang hilang akan dibiarkan sebagai nilai nol masing-masing. Jadi kita bisa katakan:
return &File{fd: fd, name: name}
Sebagai kasus pembatas, jika literal komposit tidak mengandung field sama sekali, ia menciptakan nilai nol untuk tipe tersebut. Ekspresi new(File)
dan &File{}
adalah ekuivalen.
Literal komposit juga dapat dibuat untuk array, slice, dan map, dengan label field menjadi indeks atau kunci map yang sesuai. Dalam contoh-contoh ini, inisialisasi bekerja terlepas dari nilai Enone
, Eio
, dan Einval
, selama mereka berbeda.
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
Alokasi dengan make
Kembali ke alokasi. Fungsi bawaan make(T, args)
melayani tujuan yang berbeda dari new(T)
. Ia hanya membuat slice, map, dan channel, dan mengembalikan nilai yang diinisialisasi (bukan di-nol-kan) dari tipe T
(bukan *T
). Alasan perbedaannya adalah bahwa ketiga tipe ini mewakili, di balik layar, referensi ke struktur data yang harus diinisialisasi sebelum digunakan. Sebuah slice, misalnya, adalah deskriptor tiga item yang berisi pointer ke data (di dalam array), panjang, dan kapasitas, dan sampai item-item tersebut diinisialisasi, slice tersebut adalah nil
. Untuk slice, map, dan channel, make
menginisialisasi struktur data internal dan menyiapkan nilainya untuk digunakan. Misalnya,
make([]int, 10, 100)
mengalokasikan array 100 int dan kemudian membuat struktur slice dengan panjang 10 dan kapasitas 100 yang menunjuk ke 10 elemen pertama dari array tersebut. (Saat membuat slice, kapasitas dapat dihilangkan; lihat bagian tentang slice untuk informasi lebih lanjut.) Sebaliknya, new([]int)
mengembalikan pointer ke struktur slice yang baru dialokasikan dan di-nol-kan, yaitu, pointer ke nilai slice nil
.
Contoh-contoh ini mengilustrasikan perbedaan antara new
dan make
.
var p *[]int = new([]int) // mengalokasikan struktur slice; *p == nil; jarang berguna
var v []int = make([]int, 100) // slice v sekarang merujuk ke array baru dari 100 int
// Terlalu rumit:
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatik:
v := make([]int, 100)
Ingat bahwa make
hanya berlaku untuk map, slice, dan channel dan tidak mengembalikan pointer. Untuk mendapatkan pointer eksplisit, alokasikan dengan new
atau ambil alamat variabel secara eksplisit.
Array
Array berguna saat merencanakan tata letak memori secara rinci dan terkadang dapat membantu menghindari alokasi, tetapi utamanya mereka adalah blok bangunan untuk slice, subjek dari bagian berikutnya. Untuk meletakkan dasar untuk topik itu, berikut beberapa kata tentang array.
Ada perbedaan besar antara cara kerja array di Go dan C. Di Go:
Array adalah nilai. Menugaskan satu array ke array lain akan menyalin semua elemen.
Secara khusus, jika Anda meneruskan array ke sebuah fungsi, ia akan menerima salinan dari array, bukan pointer ke sana.
Ukuran array adalah bagian dari tipenya. Tipe
[10]int
dan[20]int
adalah berbeda.
Properti nilai bisa berguna tetapi juga mahal; jika Anda menginginkan perilaku dan efisiensi seperti C, Anda bisa meneruskan pointer ke array.
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Perhatikan operator address-of eksplisit
Tetapi bahkan gaya ini pun bukan idiomatik Go. Gunakan slice sebagai gantinya.
Slice
Slice membungkus array untuk memberikan antarmuka yang lebih umum, kuat, dan nyaman untuk urutan data. Kecuali untuk item dengan dimensi eksplisit seperti matriks transformasi, sebagian besar pemrograman array di Go dilakukan dengan slice daripada array sederhana.
Slice memegang referensi ke array yang mendasarinya, dan jika Anda menugaskan satu slice ke slice lain, keduanya merujuk ke array yang sama. Jika sebuah fungsi mengambil argumen slice, perubahan yang dibuat pada elemen-elemen slice akan terlihat oleh pemanggil, analog dengan meneruskan pointer ke array yang mendasarinya. Fungsi Read
oleh karena itu dapat menerima argumen slice daripada pointer dan hitungan; panjang di dalam slice menetapkan batas atas berapa banyak data yang harus dibaca. Berikut adalah signature metode Read
dari tipe File
di paket os
:
func (f *File) Read(buf []byte) (n int, err error)
Metode ini mengembalikan jumlah byte yang dibaca dan nilai error, jika ada. Untuk membaca ke dalam 32 byte pertama dari buffer yang lebih besar buf
, slice (di sini digunakan sebagai kata kerja) buffer tersebut.
n, err := f.Read(buf[0:32])
Slicing seperti itu umum dan efisien. Faktanya, mengesampingkan efisiensi untuk saat ini, cuplikan berikut juga akan membaca 32 byte pertama dari buffer.
var n int
var err error
for i := 0; i < 32; i++ {
nbytes, e := f.Read(buf[i:i+1]) // Baca satu byte.
n += nbytes
if nbytes == 0 || e != nil {
err = e
break
}
}
Panjang slice dapat diubah selama masih muat dalam batas-batas array yang mendasarinya; cukup tugaskan ke slice dari dirinya sendiri. Kapasitas slice, yang dapat diakses oleh fungsi bawaan cap
, melaporkan panjang maksimum yang bisa dimiliki slice. Berikut adalah fungsi untuk menambahkan data ke slice. Jika data melebihi kapasitas, slice dialokasikan ulang. Slice yang dihasilkan dikembalikan. Fungsi ini menggunakan fakta bahwa len
dan cap
legal ketika diterapkan pada slice nil
, dan mengembalikan 0.
func Append(slice, data []byte) []byte {
l := len(slice)
if l + len(data) > cap(slice) { // alokasi ulang
// Alokasikan dua kali lipat dari yang dibutuhkan, untuk pertumbuhan di masa depan.
newSlice := make([]byte, (l+len(data))*2)
// Fungsi copy sudah dideklarasikan sebelumnya dan berfungsi untuk semua tipe slice.
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:l+len(data)]
copy(slice[l:], data)
return slice
}
Kita harus mengembalikan slice setelahnya karena, meskipun Append
dapat memodifikasi elemen dari slice
, slice itu sendiri (struktur data run-time yang memegang pointer, panjang, dan kapasitas) dilewatkan secara nilai.
Ide menambahkan ke slice sangat berguna sehingga ditangkap oleh fungsi bawaan append
. Namun, untuk memahami desain fungsi itu, kita perlu sedikit informasi lagi, jadi kita akan kembali ke sana nanti.
Slice Dua Dimensi (Two-dimensional slices)
Array dan slice di Go adalah satu dimensi. Untuk membuat ekuivalen dari array atau slice 2D, perlu untuk mendefinisikan array-of-array atau slice-of-slice, seperti ini:
type Transform [3][3]float64 // Array 3x3, sebenarnya array dari array.
type LinesOfText [][]byte // Slice dari byte slice.
Karena slice memiliki panjang variabel, dimungkinkan untuk setiap slice dalam memiliki panjang yang berbeda. Itu bisa menjadi situasi yang umum, seperti dalam contoh LinesOfText
kita: setiap baris memiliki panjang yang independen.
text := LinesOfText{
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party."),
}
Terkadang perlu mengalokasikan slice 2D, situasi yang bisa muncul saat memproses baris pindai piksel, misalnya. Ada dua cara untuk mencapai ini. Satu adalah mengalokasikan setiap slice secara independen; yang lain adalah mengalokasikan satu array tunggal dan mengarahkan slice-slice individual ke dalamnya. Mana yang akan digunakan tergantung pada aplikasi Anda. Jika slice mungkin tumbuh atau menyusut, mereka harus dialokasikan secara independen untuk menghindari menimpa baris berikutnya; jika tidak, bisa lebih efisien untuk membangun objek dengan satu alokasi tunggal. Sebagai referensi, berikut adalah sketsa dari kedua metode tersebut. Pertama, baris per baris:
// Alokasikan slice tingkat atas.
picture := make([][]uint8, YSize) // Satu baris per unit y.
// Ulangi baris, alokasikan slice untuk setiap baris.
for i := range picture {
picture[i] = make([]uint8, XSize)
}
Dan sekarang sebagai satu alokasi, diiris menjadi baris-baris:
// Alokasikan slice tingkat atas, sama seperti sebelumnya.
picture := make([][]uint8, YSize) // Satu baris per unit y.
// Alokasikan satu slice besar untuk menampung semua piksel.
pixels := make([]uint8, XSize*YSize) // Memiliki tipe []uint8 meskipun picture adalah [][]uint8.
// Ulangi baris, iris setiap baris dari depan slice piksel yang tersisa.
for i := range picture {
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Map
Map adalah struktur data bawaan yang nyaman dan kuat yang mengasosiasikan nilai dari satu tipe (kunci atau key) dengan nilai dari tipe lain (elemen atau value). Kunci dapat berupa tipe apa pun yang operator kesetaraannya terdefinisi, seperti integer, floating point dan bilangan kompleks, string, pointer, interface (selama tipe dinamisnya mendukung kesetaraan), struct, dan array. Slice tidak dapat digunakan sebagai kunci map, karena kesetaraan tidak terdefinisi pada mereka. Seperti slice, map memegang referensi ke struktur data yang mendasarinya. Jika Anda meneruskan map ke fungsi yang mengubah isi map, perubahan tersebut akan terlihat oleh pemanggil.
Map dapat dibangun menggunakan sintaks literal komposit biasa dengan pasangan kunci-nilai yang dipisahkan titik dua, sehingga mudah untuk membangunnya saat inisialisasi.
var timeZone = map[string]int{
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
Menugaskan dan mengambil nilai map secara sintaksis sama seperti melakukan hal yang sama untuk array dan slice kecuali bahwa indeksnya tidak harus berupa integer.
offset := timeZone["EST"]
Upaya untuk mengambil nilai map dengan kunci yang tidak ada di map akan mengembalikan nilai nol untuk tipe entri di map tersebut. Misalnya, jika map berisi integer, mencari kunci yang tidak ada akan mengembalikan 0
. Sebuah set dapat diimplementasikan sebagai map dengan tipe nilai bool
. Atur entri map ke true
untuk memasukkan nilai ke dalam set, lalu uji dengan pengindeksan sederhana.
attended := map[string]bool{
"Ann": true,
"Joe": true,
...
}
if attended[person] { // akan menjadi false jika person tidak ada di dalam map
fmt.Println(person, "was at the meeting")
}
Terkadang Anda perlu membedakan entri yang hilang dari nilai nol. Apakah ada entri untuk "UTC"
atau itu 0
karena tidak ada di dalam map sama sekali? Anda dapat membedakannya dengan bentuk penugasan ganda.
var seconds int
var ok bool
seconds, ok = timeZone[tz]
Untuk alasan yang jelas ini disebut idiom "comma ok". Dalam contoh ini, jika tz
ada, seconds
akan diatur dengan tepat dan ok
akan menjadi true; jika tidak, seconds
akan diatur ke nol dan ok
akan menjadi false. Berikut adalah fungsi yang menggabungkannya dengan laporan kesalahan yang bagus:
func offset(tz string) int {
if seconds, ok := timeZone[tz]; ok {
return seconds
}
log.Println("unknown time zone:", tz)
return 0
}
Untuk menguji keberadaan di map tanpa mengkhawatirkan nilai sebenarnya, Anda dapat menggunakan blank identifier (_
) sebagai ganti variabel biasa untuk nilainya.
_, present := timeZone[tz]
Untuk menghapus entri map, gunakan fungsi bawaan delete
, yang argumennya adalah map dan kunci yang akan dihapus. Aman untuk melakukan ini bahkan jika kuncinya sudah tidak ada di map.
delete(timeZone, "PDT") // Sekarang Waktu Standar
Pencetakan (Printing)
Pencetakan terformat di Go menggunakan gaya yang mirip dengan keluarga printf
di C tetapi lebih kaya dan lebih umum. Fungsi-fungsi ini berada di paket fmt
dan memiliki nama dengan huruf kapital: fmt.Printf
, fmt.Fprintf
, fmt.Sprintf
dan sebagainya. Fungsi string (Sprintf
, dll.) mengembalikan sebuah string daripada mengisi buffer yang disediakan.
Anda tidak perlu menyediakan string format. Untuk setiap Printf
, Fprintf
, dan Sprintf
ada sepasang fungsi lain, misalnya Print
dan Println
. Fungsi-fungsi ini tidak mengambil string format tetapi sebaliknya menghasilkan format default untuk setiap argumen. Versi Println
juga menyisipkan spasi di antara argumen dan menambahkan baris baru ke output sementara versi Print
hanya menambahkan spasi jika operan di kedua sisinya bukan string. Dalam contoh ini setiap baris menghasilkan output yang sama.
fmt.Printf("Hello %d\n", 23)
fmt.Fprint(os.Stdout, "Hello ", 23, "\n")
fmt.Println("Hello", 23)
fmt.Println(fmt.Sprint("Hello ", 23))
Fungsi cetak terformat fmt.Fprint
dan teman-temannya mengambil sebagai argumen pertama objek apa pun yang mengimplementasikan antarmuka io.Writer
; variabel os.Stdout
dan os.Stderr
adalah contoh yang sudah dikenal.
Di sini hal-hal mulai berbeda dari C. Pertama, format numerik seperti %d
tidak mengambil flag untuk tanda (positif/negatif) atau ukuran; sebaliknya, rutin pencetakan menggunakan tipe argumen untuk memutuskan properti ini.
var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
mencetak:
18446744073709551615 ffffffffffffffff; -1 -1
Jika Anda hanya ingin konversi default, seperti desimal untuk integer, Anda dapat menggunakan format serba guna %v
(untuk "value"); hasilnya persis seperti yang akan dihasilkan oleh Print
dan Println
. Selain itu, format tersebut dapat mencetak nilai apa pun, bahkan array, slice, struct, dan map. Berikut adalah pernyataan cetak untuk map zona waktu yang didefinisikan di bagian sebelumnya.
fmt.Printf("%v\n", timeZone) // atau cukup fmt.Println(timeZone)
yang memberikan output:
map[CST:-21600 EST:-18000 MST:-25200 PST:-28800 UTC:0]
Untuk map, Printf
dan teman-temannya mengurutkan output secara leksikografis berdasarkan kunci.
Saat mencetak struct, format yang dimodifikasi %+v
memberi anotasi pada field-field struktur dengan namanya, dan untuk nilai apa pun, format alternatif %#v
mencetak nilai dalam sintaks Go penuh.
type T struct {
a int
b float64
c string
}
t := &T{ 7, -2.35, "abc\tdef" }
fmt.Printf("%v\n", t)
fmt.Printf("%+v\n", t)
fmt.Printf("%#v\n", t)
fmt.Printf("%#v\n", timeZone)
mencetak:
&{7 -2.35 abc def}
&{a:7 b:-2.35 c:abc def}
&main.T{a:7, b:-2.35, c:"abc\tdef"}
map[string]int{"CST":-21600, "EST":-18000, "MST":-25200, "PST":-28800, "UTC":0}
(Perhatikan ampersand.) Format string yang dikutip itu juga tersedia melalui %q
ketika diterapkan pada nilai bertipe string
atau []byte
. Format alternatif %#q
akan menggunakan backquote sebagai gantinya jika memungkinkan. (Format %q
juga berlaku untuk integer dan rune, menghasilkan konstanta rune yang diapit tanda kutip tunggal.) Juga, %x
berfungsi pada string, array byte, dan slice byte serta pada integer, menghasilkan string heksadesimal panjang, dan dengan spasi dalam format (% x
) ia menempatkan spasi di antara byte.
Format praktis lainnya adalah %T
, yang mencetak tipe dari sebuah nilai.
fmt.Printf("%T\n", timeZone)
mencetak:
map[string]int
Jika Anda ingin mengontrol format default untuk tipe kustom, yang diperlukan hanyalah mendefinisikan metode dengan signature String() string
pada tipe tersebut. Untuk tipe sederhana T
kita, itu bisa terlihat seperti ini.
func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}
fmt.Printf("%v\n", t)
untuk mencetak dalam format:
7/-2.35/"abc\tdef"
(Jika Anda perlu mencetak nilai dari tipe T
serta pointer ke T
, receiver untuk String
harus bertipe nilai; contoh ini menggunakan pointer karena itu lebih efisien dan idiomatik untuk tipe struct. Lihat bagian di bawah tentang pointer vs. value receiver untuk informasi lebih lanjut.)
Metode String
kita dapat memanggil Sprintf
karena rutin cetak sepenuhnya reentrant dan dapat dibungkus dengan cara ini. Namun, ada satu detail penting yang harus dipahami tentang pendekatan ini: jangan membangun metode String
dengan memanggil Sprintf
dengan cara yang akan berulang ke metode String
Anda tanpa batas. Ini bisa terjadi jika panggilan Sprintf
mencoba mencetak receiver secara langsung sebagai string, yang pada gilirannya akan memanggil metode itu lagi. Ini adalah kesalahan umum dan mudah dibuat, seperti yang ditunjukkan contoh ini.
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m) // Error: akan berulang selamanya.
}
Ini juga mudah diperbaiki: konversi argumen ke tipe string dasar, yang tidak memiliki metode tersebut.
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m)) // OK: perhatikan konversi.
}
Di bagian inisialisasi kita akan melihat teknik lain yang menghindari rekursi ini.
Teknik pencetakan lain adalah meneruskan argumen rutin cetak secara langsung ke rutin lain yang sejenis. Tanda tangan Printf
menggunakan tipe ...interface{}
untuk argumen terakhirnya untuk menentukan bahwa sejumlah parameter (dari tipe apa pun) dapat muncul setelah format.
func Printf(format string, v ...interface{}) (n int, err error) {
Di dalam fungsi Printf
, v
bertindak seperti variabel bertipe []interface{}
tetapi jika diteruskan ke fungsi variadic lain, ia bertindak seperti daftar argumen biasa. Berikut adalah implementasi fungsi log.Println
yang kita gunakan di atas. Ia meneruskan argumennya langsung ke fmt.Sprintln
untuk pemformatan yang sebenarnya.
// Println mencetak ke logger standar dengan cara fmt.Println.
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...)) // Output mengambil parameter (int, string)
}
Kita menulis ...
setelah v
dalam panggilan bersarang ke Sprintln
untuk memberitahu kompiler agar memperlakukan v
sebagai daftar argumen; jika tidak, ia hanya akan meneruskan v
sebagai argumen slice tunggal.
Masih banyak lagi tentang pencetakan daripada yang telah kita bahas di sini. Lihat dokumentasi godoc
untuk paket fmt
untuk detailnya.
Ngomong-ngomong, parameter ...
bisa dari tipe tertentu, misalnya ...int
untuk fungsi min yang memilih yang terkecil dari daftar integer:
func Min(a ...int) int {
min := int(^uint(0) >> 1) // int terbesar
for _, i := range a {
if i < min {
min = i
}
}
return min
}
Append
Sekarang kita memiliki bagian yang hilang yang kita butuhkan untuk menjelaskan desain fungsi bawaan append
. Tanda tangan append
berbeda dari fungsi Append
kustom kita di atas. Secara skematis, seperti ini:
func append(slice []T, elements ...T) []T
di mana T
adalah placeholder untuk tipe apa pun. Anda tidak bisa benar-benar menulis fungsi di Go di mana tipe T
ditentukan oleh pemanggil. Itulah mengapa append
adalah fungsi bawaan: ia membutuhkan dukungan dari kompiler.
Apa yang dilakukan append
adalah menambahkan elemen-elemen ke akhir slice dan mengembalikan hasilnya. Hasilnya perlu dikembalikan karena, seperti pada Append
buatan kita, array yang mendasarinya mungkin berubah. Contoh sederhana ini:
x := []int{1,2,3}
x = append(x, 4, 5, 6)
fmt.Println(x)
mencetak [1 2 3 4 5 6]
. Jadi append
bekerja sedikit seperti Printf
, mengumpulkan sejumlah argumen yang berubah-ubah.
Tetapi bagaimana jika kita ingin melakukan apa yang dilakukan Append
kita dan menambahkan sebuah slice ke slice lain? Mudah: gunakan ...
di lokasi panggilan, sama seperti yang kita lakukan pada panggilan ke Output
di atas. Cuplikan ini menghasilkan output yang identik dengan yang di atas.
x := []int{1,2,3}
y := []int{4,5,6}
x = append(x, y...)
fmt.Println(x)
Tanpa ...
itu, kode tidak akan dikompilasi karena tipenya akan salah; y
bukan bertipe int
.
Inisialisasi (Initialization)
Meskipun secara sepintas tidak terlihat sangat berbeda dari inisialisasi di C atau C++, inisialisasi di Go lebih kuat. Struktur kompleks dapat dibangun selama inisialisasi dan masalah urutan di antara objek yang diinisialisasi, bahkan di antara paket yang berbeda, ditangani dengan benar.
Konstanta (Constants)
Konstanta di Go benar-benar konstan. Mereka dibuat pada waktu kompilasi, bahkan ketika didefinisikan sebagai lokal dalam fungsi, dan hanya bisa berupa angka, karakter (rune), string, atau boolean. Karena batasan waktu kompilasi, ekspresi yang mendefinisikannya harus berupa ekspresi konstan, yang dapat dievaluasi oleh kompiler. Misalnya, 1<<3
adalah ekspresi konstan, sementara math.Sin(math.Pi/4)
bukan karena panggilan fungsi ke math.Sin
harus terjadi pada waktu proses.
Di Go, konstanta enumerasi dibuat menggunakan enumerator iota
. Karena iota
dapat menjadi bagian dari ekspresi dan ekspresi dapat diulang secara implisit, mudah untuk membangun set nilai yang rumit.
type ByteSize float64
const (
_ = iota // abaikan nilai pertama dengan menugaskannya ke blank identifier
KB ByteSize = 1 << (10 * iota)
MB
GB
TB
PB
EB
ZB
YB
)
Kemampuan untuk melampirkan metode seperti String
ke tipe apa pun yang didefinisikan pengguna memungkinkan nilai-nilai sewenang-wenang untuk memformat diri mereka secara otomatis untuk pencetakan. Meskipun Anda akan paling sering melihatnya diterapkan pada struct, teknik ini juga berguna untuk tipe skalar seperti tipe floating-point seperti ByteSize
.
func (b ByteSize) String() string {
switch {
case b >= YB:
return fmt.Sprintf("%.2fYB", b/YB)
case b >= ZB:
return fmt.Sprintf("%.2fZB", b/ZB)
case b >= EB:
return fmt.Sprintf("%.2fEB", b/EB)
case b >= PB:
return fmt.Sprintf("%.2fPB", b/PB)
case b >= TB:
return fmt.Sprintf("%.2fTB", b/TB)
case b >= GB:
return fmt.Sprintf("%.2fGB", b/GB)
case b >= MB:
return fmt.Sprintf("%.2fMB", b/MB)
case b >= KB:
return fmt.Sprintf("%.2fKB", b/KB)
}
return fmt.Sprintf("%.2fB", b)
}
Ekspresi YB
mencetak sebagai 1.00YB
, sementara ByteSize(1e13)
mencetak sebagai 9.09TB
.
Penggunaan Sprintf
di sini untuk mengimplementasikan metode String
dari ByteSize
aman (menghindari rekursi tak terbatas) bukan karena konversi tetapi karena ia memanggil Sprintf
dengan %f
, yang bukan format string: Sprintf
hanya akan memanggil metode String
ketika ia menginginkan string, dan %f
menginginkan nilai floating-point.
Variabel (Variables)
Variabel dapat diinisialisasi sama seperti konstanta tetapi inisialisatornya bisa berupa ekspresi umum yang dihitung pada waktu proses.
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
Fungsi init
(The init function)
Akhirnya, setiap file sumber dapat mendefinisikan fungsi init
niladic (tanpa argumen) sendiri untuk menyiapkan keadaan apa pun yang diperlukan. (Sebenarnya setiap file dapat memiliki beberapa fungsi init
.) Dan akhirnya berarti benar-benar akhir: init
dipanggil setelah semua deklarasi variabel dalam paket telah mengevaluasi inisialisatornya, dan itu dievaluasi hanya setelah semua paket yang diimpor telah diinisialisasi.
Selain inisialisasi yang tidak dapat diekspresikan sebagai deklarasi, penggunaan umum dari fungsi init
adalah untuk memverifikasi atau memperbaiki kebenaran keadaan program sebelum eksekusi sebenarnya dimulai.
func init() {
if user == "" {
log.Fatal("$USER not set")
}
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath dapat diganti oleh flag --gopath pada baris perintah.
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}
Metode (Methods)
Pointer vs. Nilai (Pointers vs. Values)
Seperti yang kita lihat dengan ByteSize
, metode dapat didefinisikan untuk tipe bernama apa pun (kecuali pointer atau interface); receiver-nya tidak harus berupa struct.
Dalam pembahasan slice di atas, kita menulis fungsi Append
. Kita dapat mendefinisikannya sebagai metode pada slice. Untuk melakukan ini, kita pertama-tama mendeklarasikan tipe bernama yang dapat kita ikat metodenya, dan kemudian menjadikan receiver untuk metode tersebut sebagai nilai dari tipe itu.
type ByteSlice []byte
func (slice ByteSlice) Append(data []byte) []byte {
// Badan fungsi persis sama dengan fungsi Append yang didefinisikan di atas.
}
Ini masih mengharuskan metode untuk mengembalikan slice yang diperbarui. Kita bisa menghilangkan kekikukan itu dengan mendefinisikan ulang metode untuk mengambil pointer ke ByteSlice
sebagai receiver-nya, sehingga metode dapat menimpa slice pemanggil.
func (p *ByteSlice) Append(data []byte) {
slice := *p
// Badan fungsi seperti di atas, tanpa return.
*p = slice
}
Faktanya, kita bisa lebih baik lagi. Jika kita memodifikasi fungsi kita sehingga terlihat seperti metode Write
standar, seperti ini,
func (p *ByteSlice) Write(data []byte) (n int, err error) {
slice := *p
// Sekali lagi seperti di atas.
*p = slice
return len(data), nil
}
maka tipe *ByteSlice
memenuhi antarmuka standar io.Writer
, yang sangat berguna. Misalnya, kita bisa mencetak ke dalamnya.
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
Kita meneruskan alamat ByteSlice
karena hanya *ByteSlice
yang memenuhi io.Writer
. Aturan tentang pointer vs. nilai untuk receiver adalah bahwa metode nilai dapat dipanggil pada pointer dan nilai, tetapi metode pointer hanya dapat dipanggil pada pointer.
Aturan ini muncul karena metode pointer dapat memodifikasi receiver; memanggilnya pada sebuah nilai akan menyebabkan metode menerima salinan dari nilai tersebut, sehingga modifikasi apa pun akan dibuang. Oleh karena itu, bahasa melarang kesalahan ini. Namun, ada pengecualian yang berguna. Ketika nilainya dapat dialamatkan, bahasa menangani kasus umum memanggil metode pointer pada nilai dengan menyisipkan operator alamat secara otomatis. Dalam contoh kita, variabel b
dapat dialamatkan, jadi kita bisa memanggil metode Write
-nya hanya dengan b.Write
. Kompiler akan menulis ulang itu menjadi (&b).Write
untuk kita.
Ngomong-ngomong, ide menggunakan Write
pada slice byte adalah pusat dari implementasi bytes.Buffer
.
Interface dan Tipe Lainnya (Interfaces and other types)
Interface
Interface di Go menyediakan cara untuk menentukan perilaku sebuah objek: jika sesuatu bisa melakukan ini, maka ia bisa digunakan di sini. Kita sudah melihat beberapa contoh sederhana; pencetak kustom dapat diimplementasikan dengan metode String
sementara Fprintf
dapat menghasilkan output ke apa pun dengan metode Write
. Interface dengan hanya satu atau dua metode umum di kode Go, dan biasanya diberi nama yang berasal dari metode, seperti io.Writer
untuk sesuatu yang mengimplementasikan Write
.
Sebuah tipe dapat mengimplementasikan beberapa interface. Misalnya, sebuah koleksi dapat diurutkan oleh rutin di paket sort
jika ia mengimplementasikan sort.Interface
, yang berisi Len()
, Less(i, j int) bool
, dan Swap(i, j int)
, dan ia juga bisa memiliki pemformat kustom. Dalam contoh yang dibuat-buat ini, Sequence
memenuhi keduanya.
type Sequence []int
// Metode yang dibutuhkan oleh sort.Interface.
func (s Sequence) Len() int {
return len(s)
}
func (s Sequence) Less(i, j int) bool {
return s[i] < s[j]
}
func (s Sequence) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// Copy mengembalikan salinan dari Sequence.
func (s Sequence) Copy() Sequence {
copy := make(Sequence, 0, len(s))
return append(copy, s...)
}
// Metode untuk pencetakan - mengurutkan elemen sebelum mencetak.
func (s Sequence) String() string {
s = s.Copy() // Buat salinan; jangan timpa argumen.
sort.Sort(s)
str := "["
for i, elem := range s { // Perulangan ini O(N²); akan diperbaiki di contoh berikutnya.
if i > 0 {
str += " "
}
str += fmt.Sprint(elem)
}
return str + "]"
}
Konversi (Conversions)
Metode String
dari Sequence
menciptakan kembali pekerjaan yang sudah dilakukan Sprint
untuk slice. (Ini juga memiliki kompleksitas O(N²), yang buruk.) Kita dapat berbagi upaya (dan juga mempercepatnya) jika kita mengonversi Sequence
menjadi []int
biasa sebelum memanggil Sprint
.
func (s Sequence) String() string {
s = s.Copy()
sort.Sort(s)
return fmt.Sprint([]int(s))
}
Metode ini adalah contoh lain dari teknik konversi untuk memanggil Sprintf
dengan aman dari metode String
. Karena kedua tipe (Sequence
dan []int
) adalah sama jika kita mengabaikan nama tipenya, maka legal untuk mengonversi di antara keduanya. Konversi tidak membuat nilai baru, itu hanya sementara bertindak seolah-olah nilai yang ada memiliki tipe baru. (Ada konversi legal lainnya, seperti dari integer ke floating point, yang memang membuat nilai baru.)
Ini adalah sebuah idiom dalam program Go untuk mengonversi tipe sebuah ekspresi untuk mengakses serangkaian metode yang berbeda. Sebagai contoh, kita bisa menggunakan tipe yang ada sort.IntSlice
untuk mereduksi seluruh contoh menjadi ini:
type Sequence []int
// Metode untuk pencetakan - mengurutkan elemen sebelum mencetak
func (s Sequence) String() string {
s = s.Copy()
sort.IntSlice(s).Sort()
return fmt.Sprint([]int(s))
}
Sekarang, alih-alih membuat Sequence
mengimplementasikan beberapa interface (pengurutan dan pencetakan), kita menggunakan kemampuan item data untuk dikonversi menjadi beberapa tipe (Sequence
, sort.IntSlice
, dan []int
), yang masing-masing melakukan sebagian dari pekerjaan. Itu lebih tidak biasa dalam praktik tetapi bisa efektif.
Konversi Interface dan Type Assertion
Type switch adalah bentuk konversi: mereka mengambil sebuah interface dan, untuk setiap kasus dalam switch, dalam arti tertentu mengonversinya ke tipe dari kasus itu. Berikut adalah versi yang disederhanakan tentang bagaimana kode di bawah fmt.Printf
mengubah nilai menjadi string menggunakan type switch. Jika sudah berupa string, kita ingin nilai string aktual yang dipegang oleh interface, sementara jika memiliki metode String
, kita ingin hasil dari pemanggilan metode tersebut.
type Stringer interface {
String() string
}
var value interface{} // Nilai disediakan oleh pemanggil.
switch str := value.(type) {
case string:
return str
case Stringer:
return str.String()
}
Kasus pertama menemukan nilai konkret; yang kedua mengonversi interface menjadi interface lain. Sangat boleh untuk mencampur tipe dengan cara ini.
Bagaimana jika hanya ada satu tipe yang kita pedulikan? Jika kita tahu nilainya berisi string
dan kita hanya ingin mengekstraknya? Type switch satu kasus akan berhasil, tetapi begitu juga type assertion. Type assertion mengambil nilai interface dan mengekstrak darinya nilai dari tipe eksplisit yang ditentukan. Sintaksnya meminjam dari klausa yang membuka type switch, tetapi dengan tipe eksplisit daripada kata kunci type
:
value.(typeName)
dan hasilnya adalah nilai baru dengan tipe statis typeName
. Tipe itu harus berupa tipe konkret yang dipegang oleh interface, atau tipe interface kedua yang nilainya dapat dikonversi. Untuk mengekstrak string yang kita tahu ada di dalam nilai, kita bisa menulis:
str := value.(string)
Tetapi jika ternyata nilainya tidak berisi string, program akan mogok dengan error run-time. Untuk mencegahnya, gunakan idiom "comma, ok" untuk menguji, dengan aman, apakah nilainya adalah sebuah string:
str, ok := value.(string)
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
Jika type assertion gagal, str
akan tetap ada dan bertipe string, tetapi akan memiliki nilai nol, yaitu string kosong.
Sebagai ilustrasi kemampuan ini, berikut adalah pernyataan if-else
yang ekuivalen dengan type switch yang membuka bagian ini.
if str, ok := value.(string); ok {
return str
} else if str, ok := value.(Stringer); ok {
return str.String()
}
Generalitas (Generality)
Jika sebuah tipe ada hanya untuk mengimplementasikan sebuah interface dan tidak akan pernah memiliki metode yang diekspor di luar interface tersebut, tidak perlu mengekspor tipe itu sendiri. Mengekspor hanya interfacenya membuat jelas bahwa nilai tersebut tidak memiliki perilaku menarik di luar apa yang dijelaskan dalam interface. Ini juga menghindari kebutuhan untuk mengulangi dokumentasi pada setiap instance dari metode yang umum.
Dalam kasus seperti itu, konstruktor harus mengembalikan nilai interface daripada tipe yang mengimplementasikannya. Sebagai contoh, di pustaka hash, baik crc32.NewIEEE
maupun adler32.New
mengembalikan tipe interface hash.Hash32
. Mengganti algoritma CRC-32 dengan Adler-32 dalam program Go hanya memerlukan perubahan panggilan konstruktor; sisa kode tidak terpengaruh oleh perubahan algoritma.
Pendekatan serupa memungkinkan algoritma streaming cipher di berbagai paket crypto
dipisahkan dari block cipher yang mereka rangkai. Antarmuka Block
di paket crypto/cipher
menentukan perilaku block cipher, yang menyediakan enkripsi satu blok data. Kemudian, dengan analogi dengan paket bufio
, paket cipher yang mengimplementasikan antarmuka ini dapat digunakan untuk membangun streaming cipher, yang diwakili oleh antarmuka Stream
, tanpa mengetahui detail enkripsi blok.
Antarmuka crypto/cipher
terlihat seperti ini:
type Block interface {
BlockSize() int
Encrypt(dst, src []byte)
Decrypt(dst, src []byte)
}
type Stream interface {
XORKeyStream(dst, src []byte)
}
Berikut adalah definisi dari stream mode counter (CTR), yang mengubah block cipher menjadi streaming cipher; perhatikan bahwa detail block cipher diabstraksikan:
// NewCTR mengembalikan Stream yang mengenkripsi/mendekripsi menggunakan Block yang diberikan dalam
// mode counter. Panjang iv harus sama dengan ukuran blok Block.
func NewCTR(block Block, iv []byte) Stream
NewCTR
tidak hanya berlaku untuk satu algoritma enkripsi dan sumber data tertentu tetapi untuk implementasi apa pun dari antarmuka Block
dan Stream
apa pun. Karena mereka mengembalikan nilai interface, mengganti enkripsi CTR dengan mode enkripsi lain adalah perubahan yang terlokalisasi. Panggilan konstruktor harus diedit, tetapi karena kode di sekitarnya harus memperlakukan hasilnya hanya sebagai Stream
, ia tidak akan melihat perbedaannya.
Interface dan Metode (Interfaces and methods)
Karena hampir semua hal dapat dilampirkan metode, hampir semua hal dapat memenuhi sebuah interface. Salah satu contoh ilustratif ada di paket http
, yang mendefinisikan antarmuka Handler
. Objek apa pun yang mengimplementasikan Handler
dapat melayani permintaan HTTP.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ResponseWriter
sendiri adalah sebuah interface yang menyediakan akses ke metode yang diperlukan untuk mengembalikan respons ke klien. Metode-metode tersebut termasuk metode Write
standar, sehingga http.ResponseWriter
dapat digunakan di mana pun io.Writer
dapat digunakan. Request
adalah struct yang berisi representasi ter-parse dari permintaan dari klien.
Untuk singkatnya, mari kita abaikan POST dan asumsikan permintaan HTTP selalu GET; penyederhanaan itu tidak mempengaruhi cara handler diatur. Berikut adalah implementasi sepele dari handler untuk menghitung berapa kali halaman dikunjungi.
// Server penghitung sederhana.
type Counter struct {
n int
}
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ctr.n++
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
(Sesuai dengan tema kita, perhatikan bagaimana Fprintf
dapat mencetak ke http.ResponseWriter
.) Di server sungguhan, akses ke ctr.n
akan memerlukan perlindungan dari akses bersamaan. Lihat paket sync
dan atomic
untuk saran.
Sebagai referensi, berikut cara melampirkan server seperti itu ke sebuah node di pohon URL.
import "net/http"
...
ctr := new(Counter)
http.Handle("/counter", ctr)
Tapi mengapa membuat Counter
menjadi struct? Sebuah integer sudah cukup. (Receiver-nya perlu berupa pointer agar penambahan nilainya terlihat oleh pemanggil.)
// Server penghitung yang lebih sederhana.
type Counter int
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
*ctr++
fmt.Fprintf(w, "counter = %d\n", *ctr)
}
Bagaimana jika program Anda memiliki beberapa keadaan internal yang perlu diberitahu bahwa sebuah halaman telah dikunjungi? Ikat sebuah channel ke halaman web.
// Sebuah channel yang mengirimkan notifikasi pada setiap kunjungan.
// (Mungkin ingin channel-nya di-buffer.)
type Chan chan *http.Request
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
ch <- req
fmt.Fprint(w, "notification sent")
}
Akhirnya, katakanlah kita ingin menyajikan argumen yang digunakan saat memanggil biner server di /args
. Mudah untuk menulis fungsi untuk mencetak argumen.
func ArgServer() {
fmt.Println(os.Args)
}
Bagaimana kita mengubahnya menjadi server HTTP? Kita bisa membuat ArgServer
menjadi metode dari beberapa tipe yang nilainya kita abaikan, tetapi ada cara yang lebih bersih. Karena kita dapat mendefinisikan metode untuk tipe apa pun kecuali pointer dan interface, kita bisa menulis metode untuk sebuah fungsi. Paket http
berisi kode ini:
// Tipe HandlerFunc adalah adapter untuk memungkinkan penggunaan
// fungsi biasa sebagai handler HTTP. Jika f adalah sebuah fungsi
// dengan signature yang sesuai, HandlerFunc(f) adalah
// objek Handler yang memanggil f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP memanggil f(w, req).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) {
f(w, req)
}
HandlerFunc
adalah tipe dengan metode, ServeHTTP
, sehingga nilai dari tipe itu dapat melayani permintaan HTTP. Lihat implementasi metodenya: receiver-nya adalah sebuah fungsi, f
, dan metode tersebut memanggil f
. Itu mungkin tampak aneh tetapi tidak jauh berbeda dari, katakanlah, receiver-nya adalah sebuah channel dan metodenya mengirim ke channel tersebut.
Untuk membuat ArgServer
menjadi server HTTP, kita pertama-tama memodifikasinya agar memiliki signature yang benar.
// Server argumen.
func ArgServer(w http.ResponseWriter, req *http.Request) {
fmt.Fprintln(w, os.Args)
}
ArgServer
sekarang memiliki signature yang sama dengan HandlerFunc
, sehingga dapat dikonversi ke tipe tersebut untuk mengakses metodenya, sama seperti kita mengonversi Sequence
ke IntSlice
untuk mengakses IntSlice.Sort
. Kode untuk mengaturnya singkat:
http.Handle("/args", http.HandlerFunc(ArgServer))
Ketika seseorang mengunjungi halaman /args
, handler yang terpasang di halaman itu memiliki nilai ArgServer
dan tipe HandlerFunc
. Server HTTP akan memanggil metode ServeHTTP
dari tipe tersebut, dengan ArgServer
sebagai receiver, yang pada gilirannya akan memanggil ArgServer
(melalui pemanggilan f(w, req)
di dalam HandlerFunc.ServeHTTP
). Argumen-argumen tersebut kemudian akan ditampilkan.
Di bagian ini kita telah membuat server HTTP dari struct, integer, channel, dan fungsi, semua karena interface hanyalah sekumpulan metode, yang dapat didefinisikan untuk (hampir) semua tipe.
Blank Identifier
Kita telah menyebutkan blank identifier beberapa kali sekarang, dalam konteks perulangan for range
dan map. Blank identifier dapat ditugaskan atau dideklarasikan dengan nilai apa pun dari tipe apa pun, dengan nilai tersebut dibuang tanpa membahayakan. Ini sedikit seperti menulis ke file /dev/null
di Unix: ia mewakili nilai hanya-tulis untuk digunakan sebagai placeholder di mana sebuah variabel diperlukan tetapi nilai sebenarnya tidak relevan. Ia memiliki kegunaan di luar yang telah kita lihat.
Blank identifier dalam penugasan ganda
Penggunaan blank identifier dalam perulangan for range
adalah kasus khusus dari situasi umum: penugasan ganda.
Jika sebuah penugasan memerlukan beberapa nilai di sisi kiri, tetapi salah satu nilainya tidak akan digunakan oleh program, blank identifier di sisi kiri penugasan menghindari kebutuhan untuk membuat variabel dummy dan membuatnya jelas bahwa nilai tersebut akan dibuang. Misalnya, saat memanggil fungsi yang mengembalikan nilai dan error, tetapi hanya error yang penting, gunakan blank identifier untuk membuang nilai yang tidak relevan.
if _, err := os.Stat(path); os.IsNotExist(err) {
fmt.Printf("%s does not exist\n", path)
}
Kadang-kadang Anda akan melihat kode yang membuang nilai error untuk mengabaikan error; ini adalah praktik yang buruk. Selalu periksa nilai kembalian error; mereka disediakan karena suatu alasan.
// Buruk! Kode ini akan mogok jika path tidak ada.
fi, _ := os.Stat(path)
if fi.IsDir() {
fmt.Printf("%s is a directory\n", path)
}
Impor dan variabel yang tidak digunakan
Adalah sebuah error untuk mengimpor sebuah paket atau mendeklarasikan sebuah variabel tanpa menggunakannya. Impor yang tidak digunakan membuat program membengkak dan memperlambat kompilasi, sementara variabel yang diinisialisasi tetapi tidak digunakan setidaknya merupakan komputasi yang sia-sia dan mungkin menunjukkan bug yang lebih besar. Namun, ketika sebuah program sedang dalam pengembangan aktif, impor dan variabel yang tidak digunakan sering muncul dan bisa mengganggu untuk menghapusnya hanya agar kompilasi berjalan, hanya untuk kemudian dibutuhkan lagi. Blank identifier menyediakan solusinya.
Program setengah jadi ini memiliki dua impor yang tidak digunakan (fmt
dan io
) dan sebuah variabel yang tidak digunakan (fd
), sehingga tidak akan dikompilasi, tetapi akan bagus untuk melihat apakah kode sejauh ini sudah benar.
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: gunakan fd.
}
Untuk membungkam keluhan tentang impor yang tidak digunakan, gunakan blank identifier untuk merujuk ke sebuah simbol dari paket yang diimpor. Demikian pula, menugaskan variabel yang tidak digunakan fd
ke blank identifier akan membungkam error variabel yang tidak digunakan. Versi program ini bisa dikompilasi.
package main
import (
"fmt"
"io"
"log"
"os"
)
var _ = fmt.Printf // Untuk debugging; hapus jika sudah selesai.
var _ io.Reader // Untuk debugging; hapus jika sudah selesai.
func main() {
fd, err := os.Open("test.go")
if err != nil {
log.Fatal(err)
}
// TODO: gunakan fd.
_ = fd
}
Berdasarkan konvensi, deklarasi global untuk membungkam error impor harus diletakkan tepat setelah impor dan diberi komentar, baik untuk membuatnya mudah ditemukan maupun sebagai pengingat untuk membereskan semuanya nanti.
Impor untuk efek samping (Import for side effect)
Impor yang tidak digunakan seperti fmt
atau io
dalam contoh sebelumnya pada akhirnya harus digunakan atau dihapus: penugasan kosong mengidentifikasi kode sebagai pekerjaan yang sedang berlangsung. Tetapi terkadang berguna untuk mengimpor paket hanya untuk efek sampingnya, tanpa penggunaan eksplisit. Misalnya, selama fungsi init
-nya, paket net/http/pprof
mendaftarkan handler HTTP yang menyediakan informasi debugging. Ia memiliki API yang diekspor, tetapi sebagian besar klien hanya memerlukan pendaftaran handler dan mengakses data melalui halaman web. Untuk mengimpor paket hanya untuk efek sampingnya, ganti nama paket menjadi blank identifier:
import _ "net/http/pprof"
Bentuk impor ini menjelaskan bahwa paket tersebut diimpor untuk efek sampingnya, karena tidak ada kemungkinan penggunaan lain dari paket tersebut: di file ini, ia tidak memiliki nama. (Jika punya, dan kita tidak menggunakan nama itu, kompiler akan menolak program.)
Pemeriksaan Interface (Interface checks)
Seperti yang kita lihat dalam diskusi tentang interface di atas, sebuah tipe tidak perlu mendeklarasikan secara eksplisit bahwa ia mengimplementasikan sebuah interface. Sebaliknya, sebuah tipe mengimplementasikan interface hanya dengan mengimplementasikan metode-metode interface tersebut. Dalam praktiknya, sebagian besar konversi interface bersifat statis dan karenanya diperiksa pada waktu kompilasi. Misalnya, meneruskan *os.File
ke fungsi yang mengharapkan io.Reader
tidak akan dikompilasi kecuali *os.File
mengimplementasikan antarmuka io.Reader
.
Namun, beberapa pemeriksaan interface terjadi pada waktu proses. Salah satu contohnya adalah di paket encoding/json
, yang mendefinisikan antarmuka Marshaler
. Ketika encoder JSON menerima nilai yang mengimplementasikan antarmuka tersebut, encoder memanggil metode marshaling nilai tersebut untuk mengubahnya menjadi JSON alih-alih melakukan konversi standar. Encoder memeriksa properti ini pada waktu proses dengan type assertion seperti:
m, ok := val.(json.Marshaler)
Jika hanya perlu untuk menanyakan apakah sebuah tipe mengimplementasikan sebuah interface, tanpa benar-benar menggunakan interface itu sendiri, mungkin sebagai bagian dari pemeriksaan error, gunakan blank identifier untuk mengabaikan nilai hasil type-asserted:
if _, ok := val.(json.Marshaler); ok {
fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val)
}
Satu tempat situasi ini muncul adalah ketika perlu untuk menjamin di dalam paket yang mengimplementasikan tipe bahwa ia benar-benar memenuhi interface. Jika sebuah tipe—misalnya, json.RawMessage
—membutuhkan representasi JSON kustom, ia harus mengimplementasikan json.Marshaler
, tetapi tidak ada konversi statis yang akan menyebabkan kompiler memverifikasi ini secara otomatis. Jika tipe tersebut secara tidak sengaja gagal memenuhi interface, encoder JSON akan tetap bekerja, tetapi tidak akan menggunakan implementasi kustom. Untuk menjamin bahwa implementasinya benar, deklarasi global menggunakan blank identifier dapat digunakan di dalam paket:
var _ json.Marshaler = (*RawMessage)(nil)
Dalam deklarasi ini, penugasan yang melibatkan konversi *RawMessage
ke Marshaler
mengharuskan *RawMessage
mengimplementasikan Marshaler
, dan properti itu akan diperiksa pada waktu kompilasi. Jika antarmuka json.Marshaler
berubah, paket ini tidak akan lagi dikompilasi dan kita akan diberi tahu bahwa ia perlu diperbarui.
Kemunculan blank identifier dalam konstruksi ini menunjukkan bahwa deklarasi ada hanya untuk pemeriksaan tipe, bukan untuk membuat variabel. Namun, jangan lakukan ini untuk setiap tipe yang memenuhi sebuah interface. Berdasarkan konvensi, deklarasi seperti itu hanya digunakan ketika tidak ada konversi statis yang sudah ada dalam kode, yang merupakan kejadian langka.
Embedding
Go tidak menyediakan gagasan pewarisan (subclassing) yang digerakkan oleh tipe seperti pada umumnya, tetapi ia memiliki kemampuan untuk "meminjam" bagian-bagian dari sebuah implementasi dengan menyematkan (embedding) tipe di dalam sebuah struct atau interface.
Penyematan interface sangat sederhana. Kita telah menyebutkan antarmuka io.Reader
dan io.Writer
sebelumnya; berikut adalah definisinya.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Paket io
juga mengekspor beberapa antarmuka lain yang menentukan objek yang dapat mengimplementasikan beberapa metode tersebut. Misalnya, ada io.ReadWriter
, sebuah interface yang berisi Read
dan Write
. Kita bisa menentukan io.ReadWriter
dengan mendaftar kedua metode secara eksplisit, tetapi lebih mudah dan lebih menggugah untuk menyematkan kedua antarmuka untuk membentuk yang baru, seperti ini:
// ReadWriter adalah interface yang menggabungkan antarmuka Reader dan Writer.
type ReadWriter interface {
Reader
Writer
}
Ini mengatakan persis seperti yang terlihat: ReadWriter
dapat melakukan apa yang dilakukan Reader
dan apa yang dilakukan Writer
; ini adalah gabungan dari antarmuka yang disematkan. Hanya interface yang bisa disematkan di dalam interface.
Ide dasar yang sama berlaku untuk struct, tetapi dengan implikasi yang lebih jauh. Paket bufio
memiliki dua tipe struct, bufio.Reader
dan bufio.Writer
, yang masing-masing tentu saja mengimplementasikan antarmuka analog dari paket io
. Dan bufio
juga mengimplementasikan buffered reader/writer, yang dilakukannya dengan menggabungkan reader dan writer menjadi satu struct menggunakan penyematan: ia mendaftar tipe di dalam struct tetapi tidak memberi mereka nama field.
// ReadWriter menyimpan pointer ke Reader dan Writer.
// Ini mengimplementasikan io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
Elemen yang disematkan adalah pointer ke struct dan tentu saja harus diinisialisasi untuk menunjuk ke struct yang valid sebelum dapat digunakan. Struct ReadWriter
bisa ditulis sebagai
type ReadWriter struct {
reader *Reader
writer *Writer
}
tetapi kemudian untuk mempromosikan metode dari field dan untuk memenuhi antarmuka io
, kita juga perlu menyediakan metode penerusan (forwarding methods), seperti ini:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
Dengan menyematkan struct secara langsung, kita menghindari pencatatan ini. Metode dari tipe yang disematkan datang secara gratis, yang berarti bufio.ReadWriter
tidak hanya memiliki metode dari bufio.Reader
dan bufio.Writer
, ia juga memenuhi ketiga antarmuka: io.Reader
, io.Writer
, dan io.ReadWriter
.
Ada cara penting di mana penyematan berbeda dari pewarisan. Ketika kita menyematkan sebuah tipe, metode dari tipe itu menjadi metode dari tipe luar, tetapi ketika dipanggil, receiver dari metode tersebut adalah tipe dalam, bukan tipe luar. Dalam contoh kita, ketika metode Read
dari bufio.ReadWriter
dipanggil, ia memiliki efek yang persis sama dengan metode penerusan yang ditulis di atas; receiver-nya adalah field reader
dari ReadWriter
, bukan ReadWriter
itu sendiri.
Penyematan juga bisa menjadi kenyamanan sederhana. Contoh ini menunjukkan field yang disematkan di samping field biasa yang bernama.
type Job struct {
Command string
*log.Logger
}
Tipe Job
sekarang memiliki metode Print
, Printf
, Println
dan lainnya dari *log.Logger
. Tentu saja kita bisa memberi Logger
nama field, tetapi tidak perlu melakukannya. Dan sekarang, setelah diinisialisasi, kita bisa melakukan logging ke Job
:
job.Println("starting now...")
Logger
adalah field biasa dari struct Job
, jadi kita bisa menginisialisasinya dengan cara biasa di dalam konstruktor untuk Job
, seperti ini,
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
atau dengan literal komposit,
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
Jika kita perlu merujuk ke field yang disematkan secara langsung, nama tipe dari field tersebut, dengan mengabaikan kualifikasi paket, berfungsi sebagai nama field, seperti yang terjadi pada metode Read
dari struct ReadWriter
kita. Di sini, jika kita perlu mengakses *log.Logger
dari variabel job
bertipe Job
, kita akan menulis job.Logger
, yang akan berguna jika kita ingin menyempurnakan metode dari Logger
.
func (job *Job) Printf(format string, args ...interface{}) {
job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}
Penyematan tipe memperkenalkan masalah konflik nama tetapi aturan untuk menyelesaikannya sederhana. Pertama, field atau metode X
menyembunyikan item X
lain di bagian yang lebih dalam dari tipe. Jika log.Logger
berisi field atau metode bernama Command
, field Command
dari Job
akan mendominasinya.
Kedua, jika nama yang sama muncul pada tingkat penyarangan yang sama, biasanya itu adalah error; akan keliru untuk menyematkan log.Logger
jika struct Job
berisi field atau metode lain yang disebut Logger
. Namun, jika nama duplikat tersebut tidak pernah disebutkan dalam program di luar definisi tipe, itu tidak apa-apa. Kualifikasi ini memberikan perlindungan terhadap perubahan yang dibuat pada tipe yang disematkan dari luar; tidak ada masalah jika sebuah field ditambahkan yang berkonflik dengan field lain di subtipe lain jika tidak ada field yang pernah digunakan.
Konkurensi (Concurrency)
Berbagi dengan Berkomunikasi (Share by communicating)
Pemrograman konkuren adalah topik besar dan di sini hanya ada ruang untuk beberapa sorotan spesifik Go.
Pemrograman konkuren di banyak lingkungan dibuat sulit oleh seluk-beluk yang diperlukan untuk mengimplementasikan akses yang benar ke variabel bersama. Go mendorong pendekatan yang berbeda di mana nilai-nilai bersama dilewatkan melalui channel dan, pada kenyataannya, tidak pernah dibagikan secara aktif oleh utas eksekusi yang terpisah. Hanya satu goroutine yang memiliki akses ke nilai pada waktu tertentu. Data race tidak dapat terjadi, secara desain. Untuk mendorong cara berpikir ini, kami telah mereduksinya menjadi sebuah slogan:
Jangan berkomunikasi dengan berbagi memori; sebaliknya, berbagilah memori dengan berkomunikasi.
Pendekatan ini bisa terlalu jauh. Penghitungan referensi (reference counts) mungkin paling baik dilakukan dengan menempatkan mutex di sekitar variabel integer, misalnya. Tetapi sebagai pendekatan tingkat tinggi, menggunakan channel untuk mengontrol akses membuatnya lebih mudah untuk menulis program yang jelas dan benar.
Salah satu cara untuk memikirkan model ini adalah dengan mempertimbangkan program berutas tunggal (single-threaded) yang khas berjalan pada satu CPU. Ia tidak memerlukan primitif sinkronisasi. Sekarang jalankan instance lain seperti itu; ia juga tidak memerlukan sinkronisasi. Sekarang biarkan keduanya berkomunikasi; jika komunikasi adalah sinkronisatornya, masih tidak perlu sinkronisasi lain. Unix pipeline, misalnya, sangat cocok dengan model ini. Meskipun pendekatan Go terhadap konkurensi berasal dari Communicating Sequential Processes (CSP) Hoare, ia juga dapat dilihat sebagai generalisasi Unix pipes yang aman-tipe (type-safe).
Goroutine
Mereka disebut goroutine karena istilah yang ada—thread, coroutine, proses, dan sebagainya—menyampaikan konotasi yang tidak akurat. Sebuah goroutine memiliki model sederhana: ia adalah sebuah fungsi yang dieksekusi secara bersamaan dengan goroutine lain di ruang alamat yang sama. Ia ringan, biayanya sedikit lebih dari alokasi ruang tumpukan (stack). Dan tumpukannya dimulai dari ukuran kecil, jadi murah, dan tumbuh dengan mengalokasikan (dan membebaskan) penyimpanan heap sesuai kebutuhan.
Goroutine dimultipleks ke beberapa utas OS sehingga jika satu terhalang, seperti saat menunggu I/O, yang lain terus berjalan. Desain mereka menyembunyikan banyak kerumitan pembuatan dan manajemen utas.
Awali panggilan fungsi atau metode dengan kata kunci go
untuk menjalankan panggilan tersebut di goroutine baru. Ketika panggilan selesai, goroutine keluar, secara diam-diam. (Efeknya mirip dengan notasi &
di shell Unix untuk menjalankan perintah di latar belakang.)
go list.Sort() // jalankan list.Sort secara bersamaan; jangan menunggunya.
Literal fungsi bisa berguna dalam pemanggilan goroutine.
func Announce(message string, delay time.Duration) {
go func() {
time.Sleep(delay)
fmt.Println(message)
}() // Perhatikan tanda kurung - harus memanggil fungsi.
}
Di Go, literal fungsi adalah closure: implementasinya memastikan variabel yang dirujuk oleh fungsi bertahan selama mereka aktif.
Contoh-contoh ini tidak terlalu praktis karena fungsi-fungsi tersebut tidak memiliki cara untuk memberi sinyal penyelesaian. Untuk itu, kita perlu channel.
Channel
Seperti map, channel dialokasikan dengan make
, dan nilai yang dihasilkan bertindak sebagai referensi ke struktur data yang mendasarinya. Jika parameter integer opsional disediakan, itu mengatur ukuran buffer untuk channel. Defaultnya adalah nol, untuk channel tanpa buffer atau sinkron.
ci := make(chan int) // channel integer tanpa buffer
cj := make(chan int, 0) // channel integer tanpa buffer
cs := make(chan *os.File, 100) // channel pointer ke File dengan buffer 100
Channel tanpa buffer menggabungkan komunikasi—pertukaran nilai—dengan sinkronisasi—menjamin bahwa dua perhitungan (goroutine) berada dalam keadaan yang diketahui.
Ada banyak idiom bagus menggunakan channel. Berikut satu untuk memulai. Di bagian sebelumnya kita meluncurkan pengurutan di latar belakang. Sebuah channel dapat memungkinkan goroutine peluncur untuk menunggu pengurutan selesai.
c := make(chan int) // Alokasikan sebuah channel.
// Mulai pengurutan di sebuah goroutine; ketika selesai, beri sinyal di channel.
go func() {
list.Sort()
c <- 1 // Kirim sinyal; nilainya tidak penting.
}()
doSomethingForAWhile()
<-c // Tunggu pengurutan selesai; buang nilai yang dikirim.
Penerima selalu memblokir sampai ada data untuk diterima. Jika channel tidak di-buffer, pengirim memblokir sampai penerima telah menerima nilainya. Jika channel memiliki buffer, pengirim hanya memblokir sampai nilainya telah disalin ke buffer; jika buffer penuh, ini berarti menunggu sampai beberapa penerima telah mengambil nilai.
Channel dengan buffer dapat digunakan seperti semaphore, misalnya untuk membatasi throughput. Dalam contoh ini, permintaan yang masuk diteruskan ke handle
, yang mengirimkan nilai ke channel, memproses permintaan, dan kemudian menerima nilai dari channel untuk menyiapkan "semaphore" untuk konsumen berikutnya. Kapasitas buffer channel membatasi jumlah panggilan simultan ke process
.
var sem = make(chan int, MaxOutstanding)
func handle(r *Request) {
sem <- 1 // Tunggu antrian aktif tiris.
process(r) // Mungkin memakan waktu lama.
<-sem // Selesai; aktifkan permintaan berikutnya untuk berjalan.
}
func Serve(queue chan *Request) {
for {
req := <-queue
go handle(req) // Jangan tunggu handle selesai.
}
}
Setelah MaxOutstanding
handler mengeksekusi process
, handler lain akan memblokir saat mencoba mengirim ke buffer channel yang penuh, sampai salah satu handler yang ada selesai dan menerima dari buffer.
Desain ini memiliki masalah: Serve
membuat goroutine baru untuk setiap permintaan yang masuk, meskipun hanya MaxOutstanding
dari mereka yang dapat berjalan pada saat tertentu. Akibatnya, program dapat mengonsumsi sumber daya tak terbatas jika permintaan datang terlalu cepat. Kita dapat mengatasi kekurangan itu dengan mengubah Serve
untuk mengatur pembuatan goroutine:
func Serve(queue chan *Request) {
for req := range queue {
sem <- 1
go func() {
process(req)
<-sem
}()
}
}
(Catatan: pada versi Go sebelum 1.22, kode ini memiliki bug: variabel perulangan dibagikan di semua goroutine. Lihat Go wiki untuk detailnya.)
Pendekatan lain yang mengelola sumber daya dengan baik adalah dengan memulai sejumlah tetap goroutine handle
yang semuanya membaca dari channel permintaan. Jumlah goroutine membatasi jumlah panggilan simultan ke process
. Fungsi Serve
ini juga menerima sebuah channel di mana ia akan diberi tahu untuk keluar; setelah meluncurkan goroutine, ia memblokir menerima dari channel itu.
func handle(queue chan *Request) {
for r := range queue {
process(r)
}
}
func Serve(clientRequests chan *Request, quit chan bool) {
// Mulai handler
for i := 0; i < MaxOutstanding; i++ {
go handle(clientRequests)
}
<-quit // Tunggu untuk diberitahu keluar.
}
Channel dari Channel (Channels of channels)
Salah satu properti terpenting dari Go adalah bahwa channel adalah nilai kelas satu yang dapat dialokasikan dan dilewatkan seperti nilai lainnya. Penggunaan umum dari properti ini adalah untuk mengimplementasikan demultiplexing paralel yang aman.
Dalam contoh di bagian sebelumnya, handle
adalah handler yang diidealkan untuk sebuah permintaan tetapi kita tidak mendefinisikan tipe yang ditanganinya. Jika tipe itu menyertakan sebuah channel untuk membalas, setiap klien dapat menyediakan jalurnya sendiri untuk jawaban. Berikut adalah definisi skematis dari tipe Request
.
type Request struct {
args []int
f func([]int) int
resultChan chan int
}
Klien menyediakan sebuah fungsi dan argumennya, serta sebuah channel di dalam objek permintaan untuk menerima jawaban.
func sum(a []int) (s int) {
for _, v := range a {
s += v
}
return
}
request := &Request{[]int{3, 4, 5}, sum, make(chan int)}
// Kirim permintaan
clientRequests <- request
// Tunggu respons.
fmt.Printf("jawaban: %d\n", <-request.resultChan)
Di sisi server, fungsi handler adalah satu-satunya yang berubah.
func handle(queue chan *Request) {
for req := range queue {
req.resultChan <- req.f(req.args)
}
}
Jelas ada lebih banyak yang harus dilakukan untuk membuatnya realistis, tetapi kode ini adalah kerangka kerja untuk sistem RPC yang rate-limited, paralel, non-blocking, dan tidak ada satupun mutex yang terlihat.
Paralelisasi (Parallelization)
Aplikasi lain dari ide-ide ini adalah untuk memparalelkan perhitungan di beberapa inti CPU. Jika perhitungan dapat dipecah menjadi bagian-bagian terpisah yang dapat dieksekusi secara independen, ia dapat diparalelkan, dengan sebuah channel untuk memberi sinyal ketika setiap bagian selesai.
Katakanlah kita memiliki operasi mahal untuk dilakukan pada sebuah vektor item, dan nilai operasi pada setiap item bersifat independen, seperti dalam contoh yang diidealkan ini.
type Vector []float64
// Terapkan operasi ke v[i], v[i+1] ... hingga v[n-1].
func (v Vector) DoSome(i, n int, u Vector, c chan int) {
for ; i < n; i++ {
v[i] += u.Op(v[i])
}
c <- 1 // sinyal bahwa bagian ini selesai
}
Kita meluncurkan bagian-bagian tersebut secara independen dalam sebuah perulangan, satu per CPU. Mereka bisa selesai dalam urutan apa pun tetapi itu tidak masalah; kita hanya menghitung sinyal penyelesaian dengan menguras channel setelah meluncurkan semua goroutine.
const numCPU = 4 // jumlah inti CPU
func (v Vector) DoAll(u Vector) {
c := make(chan int, numCPU) // Buffering opsional tapi masuk akal.
for i := 0; i < numCPU; i++ {
go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c)
}
// Kuras channel.
for i := 0; i < numCPU; i++ {
<-c // tunggu satu tugas selesai
}
// Semua selesai.
}
Daripada membuat nilai konstan untuk numCPU, kita bisa bertanya pada runtime nilai apa yang sesuai. Fungsi runtime.NumCPU
mengembalikan jumlah inti CPU perangkat keras di mesin, jadi kita bisa menulis:
var numCPU = runtime.NumCPU()
Ada juga fungsi runtime.GOMAXPROCS
, yang melaporkan (atau mengatur) jumlah inti yang ditentukan pengguna yang dapat dijalankan oleh program Go secara bersamaan. Defaultnya adalah nilai runtime.NumCPU
tetapi dapat diganti dengan mengatur variabel lingkungan shell dengan nama serupa atau dengan memanggil fungsi dengan angka positif. Memanggilnya dengan nol hanya menanyakan nilainya. Oleh karena itu jika kita ingin menghormati permintaan sumber daya pengguna, kita harus menulis:
var numCPU = runtime.GOMAXPROCS(0)
Pastikan untuk tidak membingungkan ide konkurensi—menyusun program sebagai komponen yang dieksekusi secara independen—dan paralelisme—mengeksekusi perhitungan secara paralel untuk efisiensi pada beberapa CPU. Meskipun fitur konkurensi Go dapat membuat beberapa masalah mudah distrukturkan sebagai komputasi paralel, Go adalah bahasa konkuren, bukan bahasa paralel, dan tidak semua masalah paralelisasi cocok dengan model Go. Untuk diskusi tentang perbedaannya, lihat ceramah yang dikutip di postingan blog ini.
Buffer yang Bocor (A leaky buffer)
Alat-alat pemrograman konkuren bahkan dapat membuat ide-ide non-konkuren lebih mudah diungkapkan. Berikut adalah contoh yang diabstraksikan dari paket RPC. Goroutine klien berulang kali menerima data dari beberapa sumber, mungkin jaringan. Untuk menghindari alokasi dan pembebasan buffer, ia menyimpan daftar bebas (free list), dan menggunakan channel dengan buffer untuk mewakilinya. Jika channel kosong, buffer baru dialokasikan. Setelah buffer pesan siap, ia dikirim ke server di serverChan
.
var freeList = make(chan *Buffer, 100)
var serverChan = make(chan *Buffer)
func client() {
for {
var b *Buffer
// Ambil buffer jika tersedia; alokasikan jika tidak.
select {
case b = <-freeList:
// Dapat satu; tidak ada lagi yang harus dilakukan.
default:
// Tidak ada yang bebas, jadi alokasikan yang baru.
b = new(Buffer)
}
load(b) // Baca pesan berikutnya dari jaringan.
serverChan <- b // Kirim ke server.
}
}
Perulangan server menerima setiap pesan dari klien, memprosesnya, dan mengembalikan buffer ke daftar bebas.
func server() {
for {
b := <-serverChan // Tunggu pekerjaan.
process(b)
// Gunakan kembali buffer jika ada ruang.
select {
case freeList <- b:
// Buffer di daftar bebas; tidak ada lagi yang harus dilakukan.
default:
// Daftar bebas penuh, lanjutkan saja.
}
}
}
Klien mencoba mengambil buffer dari freeList
; jika tidak ada yang tersedia, ia mengalokasikan yang baru. Pengiriman server ke freeList
menempatkan b
kembali ke daftar bebas kecuali daftar tersebut penuh, dalam hal ini buffer dibiarkan begitu saja untuk diambil kembali oleh garbage collector. (Klausa default
dalam pernyataan select
dieksekusi ketika tidak ada kasus lain yang siap, yang berarti select
tidak pernah memblokir.) Implementasi ini membangun daftar bebas leaky bucket hanya dalam beberapa baris, mengandalkan channel dengan buffer dan garbage collector untuk pembukuan.
Error
Rutin pustaka seringkali harus mengembalikan semacam indikasi kesalahan kepada pemanggil. Seperti yang disebutkan sebelumnya, pengembalian nilai ganda Go memudahkan untuk mengembalikan deskripsi kesalahan terperinci di samping nilai kembalian normal. Merupakan gaya yang baik untuk menggunakan fitur ini untuk memberikan informasi kesalahan yang terperinci. Misalnya, seperti yang akan kita lihat, os.Open
tidak hanya mengembalikan pointer nil
saat gagal, ia juga mengembalikan nilai error yang menjelaskan apa yang salah.
Berdasarkan konvensi, error memiliki tipe error
, sebuah interface bawaan yang sederhana.
type error interface {
Error() string
}
Penulis pustaka bebas mengimplementasikan interface ini dengan model yang lebih kaya di baliknya, memungkinkan tidak hanya untuk melihat error tetapi juga untuk memberikan beberapa konteks. Seperti yang disebutkan, di samping nilai kembalian *os.File
yang biasa, os.Open
juga mengembalikan nilai error. Jika file berhasil dibuka, error akan menjadi nil
, tetapi ketika ada masalah, ia akan menampung os.PathError
:
// PathError mencatat sebuah error dan operasi serta
// path file yang menyebabkannya.
type PathError struct {
Op string // "open", "unlink", dll.
Path string // File yang terkait.
Err error // Dikembalikan oleh panggilan sistem.
}
func (e *PathError) Error() string {
return e.Op + " " + e.Path + ": " + e.Err.Error()
}
Error
dari PathError
menghasilkan string seperti ini:
open /etc/passwx: no such file or directory
Error seperti itu, yang mencakup nama file yang bermasalah, operasi, dan error sistem operasi yang dipicunya, berguna bahkan jika dicetak jauh dari panggilan yang menyebabkannya; ini jauh lebih informatif daripada "no such file or directory" biasa.
Jika memungkinkan, string error harus mengidentifikasi asalnya, seperti dengan memiliki awalan yang menamai operasi atau paket yang menghasilkan error. Misalnya, di paket image
, representasi string untuk error dekode karena format yang tidak dikenal adalah "image: unknown format".
Pemanggil yang peduli dengan detail error yang tepat dapat menggunakan type switch atau type assertion untuk mencari error spesifik dan mengekstrak detail. Untuk PathError
, ini mungkin termasuk memeriksa field Err
internal untuk kegagalan yang dapat dipulihkan.
for try := 0; try < 2; try++ {
file, err = os.Create(filename)
if err == nil {
return
}
if e, ok := err.(*os.PathError); ok && e.Err == syscall.ENOSPC {
deleteTempFiles() // Pulihkan beberapa ruang.
continue
}
return
}
Pernyataan if
kedua di sini adalah type assertion lain. Jika gagal, ok
akan menjadi false, dan e
akan menjadi nil
. Jika berhasil, ok
akan menjadi true, yang berarti errornya bertipe *os.PathError
, dan begitu juga e
, yang bisa kita periksa untuk informasi lebih lanjut tentang error tersebut.
Panic
Cara biasa untuk melaporkan error ke pemanggil adalah dengan mengembalikan error
sebagai nilai kembalian tambahan. Metode Read
kanonis adalah contoh yang terkenal; ia mengembalikan hitungan byte dan sebuah error
. Tetapi bagaimana jika error tidak dapat dipulihkan? Terkadang program просто tidak dapat melanjutkan.
Untuk tujuan ini, ada fungsi bawaan panic
yang pada dasarnya menciptakan error run-time yang akan menghentikan program (tetapi lihat bagian berikutnya). Fungsi ini mengambil satu argumen dari tipe apa pun—seringkali sebuah string—untuk dicetak saat program mati. Ini juga merupakan cara untuk menunjukkan bahwa sesuatu yang mustahil telah terjadi, seperti keluar dari perulangan tak terbatas.
// Implementasi mainan dari akar pangkat tiga menggunakan metode Newton.
func CubeRoot(x float64) float64 {
z := x/3 // Nilai awal sewenang-wenang
for i := 0; i < 1e6; i++ {
prevz := z
z -= (z*z*z-x) / (3*z*z)
if veryClose(z, prevz) {
return z
}
}
// Satu juta iterasi belum konvergen; ada yang salah.
panic(fmt.Sprintf("CubeRoot(%g) did not converge", x))
}
Ini hanya sebuah contoh tetapi fungsi pustaka yang sebenarnya harus menghindari panic
. Jika masalahnya bisa ditutupi atau diatasi, selalu lebih baik membiarkan program terus berjalan daripada mematikan seluruh program. Salah satu kemungkinan pengecualian adalah selama inisialisasi: jika pustaka benar-benar tidak dapat mengatur dirinya sendiri, mungkin masuk akal untuk panic
.
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}
Recover
Ketika panic
dipanggil, termasuk secara implisit untuk error run-time seperti pengindeksan slice di luar batas atau kegagalan type assertion, ia segera menghentikan eksekusi fungsi saat ini dan mulai membuka tumpukan (unwinding the stack) dari goroutine, menjalankan fungsi defer
apa pun di sepanjang jalan. Jika pembukaan tumpukan itu mencapai puncak tumpukan goroutine, program akan mati. Namun, dimungkinkan untuk menggunakan fungsi bawaan recover
untuk mendapatkan kembali kontrol atas goroutine dan melanjutkan eksekusi normal.
Panggilan ke recover
menghentikan pembukaan tumpukan dan mengembalikan argumen yang dilewatkan ke panic
. Karena satu-satunya kode yang berjalan saat pembukaan tumpukan berada di dalam fungsi defer
, recover
hanya berguna di dalam fungsi defer
.
Salah satu aplikasi recover
adalah untuk mematikan goroutine yang gagal di dalam server tanpa membunuh goroutine lain yang sedang dieksekusi.
func server(workChan <-chan *Work) {
for work := range workChan {
go safelyDo(work)
}
}
func safelyDo(work *Work) {
defer func() {
if err := recover(); err != nil {
log.Println("work failed:", err)
}
}()
do(work)
}
Dalam contoh ini, jika do(work)
mengalami panic
, hasilnya akan dicatat dan goroutine akan keluar dengan bersih tanpa mengganggu yang lain. Tidak perlu melakukan hal lain di dalam closure yang ditunda; memanggil recover
menangani kondisi sepenuhnya.
Karena recover
selalu mengembalikan nil
kecuali dipanggil langsung dari fungsi defer
, kode yang ditunda dapat memanggil rutin pustaka yang sendirinya menggunakan panic
dan recover
tanpa gagal. Sebagai contoh, fungsi defer
di safelyDo
mungkin memanggil fungsi logging sebelum memanggil recover
, dan kode logging itu akan berjalan tanpa terpengaruh oleh keadaan panik.
Dengan pola pemulihan kita, fungsi do
(dan apa pun yang dipanggilnya) dapat keluar dari situasi buruk apa pun dengan bersih dengan memanggil panic
. Kita bisa menggunakan ide itu untuk menyederhanakan penanganan error di perangkat lunak yang kompleks. Mari kita lihat versi ideal dari paket regexp
, yang melaporkan error parsing dengan memanggil panic
dengan tipe error lokal. Berikut adalah definisi dari Error
, sebuah metode error
, dan fungsi Compile
.
// Error adalah tipe dari parse error; ia memenuhi interface error.
type Error string
func (e Error) Error() string {
return string(e)
}
// error adalah metode dari *Regexp yang melaporkan error parsing dengan
// panicking dengan sebuah Error.
func (regexp *Regexp) error(err string) {
panic(Error(err))
}
// Compile mengembalikan representasi ter-parse dari ekspresi reguler.
func Compile(str string) (regexp *Regexp, err error) {
regexp = new(Regexp)
// doParse akan panic jika ada parse error.
defer func() {
if e := recover(); e != nil {
regexp = nil // Kosongkan nilai kembalian.
err = e.(Error) // Akan re-panic jika bukan parse error.
}
}()
return regexp.doParse(str), nil
}
Jika doParse
mengalami panic
, blok pemulihan akan mengatur nilai kembalian menjadi nil
—fungsi defer
dapat memodifikasi nilai kembalian yang dinamai. Kemudian ia akan memeriksa, dalam penugasan ke err
, bahwa masalahnya adalah error parsing dengan menegaskan bahwa ia memiliki tipe lokal Error
. Jika tidak, type assertion akan gagal, menyebabkan error run-time yang melanjutkan pembukaan tumpukan seolah-olah tidak ada yang menginterupsinya. Pemeriksaan ini berarti bahwa jika sesuatu yang tidak terduga terjadi, seperti indeks di luar batas, kode akan gagal meskipun kita menggunakan panic
dan recover
untuk menangani error parsing.
Dengan penanganan error di tempatnya, metode error
(karena ini adalah metode yang terikat pada sebuah tipe, tidak apa-apa, bahkan wajar, jika memiliki nama yang sama dengan tipe error
bawaan) memudahkan untuk melaporkan error parsing tanpa khawatir tentang membuka tumpukan parse secara manual:
if pos == 0 {
re.error("'*' ilegal di awal ekspresi")
}
Meskipun pola ini berguna, ia hanya boleh digunakan di dalam sebuah paket. Parse
mengubah panggilan panic
internalnya menjadi nilai error
; ia tidak mengekspos panic
ke kliennya. Itu adalah aturan yang baik untuk diikuti.
Ngomong-ngomong, idiom re-panic ini mengubah nilai panic
jika terjadi error yang sebenarnya. Namun, baik kegagalan asli maupun yang baru akan disajikan dalam laporan crash, sehingga akar penyebab masalah akan tetap terlihat. Dengan demikian, pendekatan re-panic sederhana ini biasanya cukup—ini adalah crash—tetapi jika Anda ingin menampilkan hanya nilai asli, Anda dapat menulis sedikit lebih banyak kode untuk menyaring masalah yang tidak terduga dan melakukan re-panic dengan error asli. Itu disisakan sebagai latihan untuk pembaca.
Server Web (A web server)
Mari kita akhiri dengan program Go yang lengkap, sebuah server web. Yang ini sebenarnya semacam re-server web. Google menyediakan layanan di chart.apis.google.com
yang melakukan pemformatan data otomatis menjadi bagan dan grafik. Namun, sulit untuk digunakan secara interaktif, karena Anda perlu memasukkan data ke dalam URL sebagai kueri. Program di sini menyediakan antarmuka yang lebih baik untuk satu bentuk data: diberikan sepotong teks pendek, ia memanggil server bagan untuk menghasilkan kode QR, matriks kotak yang mengkodekan teks. Gambar itu dapat diambil dengan kamera ponsel Anda dan diinterpretasikan sebagai, misalnya, URL, menghemat Anda mengetik URL ke keyboard kecil ponsel.
Berikut adalah program lengkapnya. Penjelasan menyusul.
package main
import (
"flag"
"html/template"
"log"
"net/http"
)
var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18
var templ = template.Must(template.New("qr").Parse(templateStr))
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(QR))
err := http.ListenAndServe(*addr, nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func QR(w http.ResponseWriter, req *http.Request) {
templ.Execute(w, req.FormValue("s"))
}
const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
<input maxLength=1024 size=70 name=s value="" title="Text to QR Encode">
<input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`
Bagian-bagian hingga main
seharusnya mudah diikuti. Satu flag mengatur port HTTP default untuk server kita. Variabel template templ
adalah tempat kesenangan terjadi. Ia membangun template HTML yang akan dieksekusi oleh server untuk menampilkan halaman; lebih lanjut tentang itu sebentar lagi.
Fungsi main
mem-parsing flag dan, menggunakan mekanisme yang kita bicarakan di atas, mengikat fungsi QR
ke path root untuk server. Kemudian http.ListenAndServe
dipanggil untuk memulai server; ia memblokir saat server berjalan.
QR
hanya menerima permintaan, yang berisi data formulir, dan mengeksekusi template pada data dalam nilai formulir bernama s
.
Paket template html/template
sangat kuat; program ini hanya menyentuh kemampuannya. Intinya, ia menulis ulang sepotong teks HTML secara dinamis dengan mengganti elemen yang berasal dari item data yang diteruskan ke templ.Execute
, dalam hal ini nilai formulir. Di dalam teks template (templateStr
), potongan yang diapit kurung kurawal ganda menunjukkan tindakan template. Potongan dari {{if .}}
hingga {{end}}
hanya dieksekusi jika nilai item data saat ini, yang disebut .
(titik), tidak kosong. Artinya, ketika string kosong, potongan template ini ditekan.
Dua cuplikan {{.}}
mengatakan untuk menampilkan data yang disajikan ke template—string kueri—di halaman web. Paket template HTML secara otomatis menyediakan escaping yang sesuai sehingga teks aman untuk ditampilkan.
Sisa dari string template hanyalah HTML untuk ditampilkan saat halaman dimuat. Jika penjelasan ini terlalu cepat, lihat dokumentasi untuk paket template untuk diskusi yang lebih mendalam.
Dan begitulah: server web yang berguna dalam beberapa baris kode ditambah beberapa teks HTML yang digerakkan oleh data. Go cukup kuat untuk membuat banyak hal terjadi dalam beberapa baris.
Subscribe to my newsletter
Read articles from Nabil ulil Albab directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
