Pendahuluan
Go adalah bahasa baru. Meskipun ia membawa gagasan dari bahasa-bahasa pemrograman yang sudah ada, ia memiliki beberapa properti yang tidak umum yang membuat program Go yang efektif berbeda karakternya dengan program yang ditulis dengan bahasa lain yang mirip. Saduran langsung dari sebuah program yang ditulis dengan C++ atau Java ke Go kemungkinan menghasilkan program yang kurang memuaskan. Di sisi lain, memikirkan solusi dari sebuah masalah dengan perspektif Go bisa menghasilkan program yang baik tapi sedikit berbeda. Dengan kata lain, untuk menulis Go dengan benar, sangat penting untuk memahami properti dan idiomnya. Perlu juga diketahui konvensi umum pemrograman dalam Go, seperti penamaan, pemformatan, konstruksi program, dan lainnya, sehingga program Go yang kita buat dapat dengan mudah dibaca oleh pemrogram Go yang lain.
Dokumen ini memberikan beberapa petunjuk untuk menulis kode Go yang bersih dan idiomatis. Ia menggabungkan spesifikasi dari bahasa Go, Tur bahasa Go, dan Cara menulis kode Go, yang mana sebaiknya harus dibaca terlebih dahulu.
Contoh
Sumber paket Go bertujuan untuk menyediakan tidak hanya pustaka inti tapi juga sebagai contoh bagaimana menggunakan bahasa ini. Lebih lanjut lagi, banyak dari paket-paket tersebut memiliki contoh-contoh kode yang dapat dieksekusi yang dapat dijalankan secara langsung, seperti yang satu ini (jika perlu, klik pada kata "Example" untuk membukanya). Jika ada pertanyaan tentang bagaimana menyelesaikan suatu masalah atau bagaimana mengimplementasikan sesuatu, dokumentasi ini, kode, dan contoh-contoh dalam pustaka dapat menyediakan jawaban, gagasan dan latar belakang yang cukup baik.
Pemformatan
Masalah pemformatan adalah yang paling kontroversial tapi juga yang paling tidak penting. Orang-orang dapat beradaptasi dengan gaya pemformatan yang berbeda-beda tapi lebih baik jika mereka tidak perlu memikirkannya, dan lebih sedikit waktu yang terpakai untuk membahasnya jika semua orang menganut gaya yang sama. Masalahnya adalah bagaimana mencapai Utopia ini tanpa memerlukan sebuah panduan preskriptif yang panjang.
Dengan Go kita mengambil pendekatan yang tidak umum dan menyerahkan pada mesin
masalah pemformatan ini.
Program gofmt
(juga tersedia lewat go fmt
, yang beroperasi dalam level
paket bukan dalam level berkas kode) membaca sebuah program Go dan
mengeluarkan kode sumber dengan standar identasi, dengan tetap menjaga dan
bila perlu memformat bagian komentar.
Jika ingin mengetahui bagaimana cara menangani tata letak sebuah bagian
kode, jalankan gofmt
; jika keluarannya tampak tidak benar, atur ulang sumber
kode (atau laporkan sebagai bug dari gofmt
).
Sebagai contoh, tidak perlu menghabiskan waktu mensejajarkan bagian komentar
pada field dari sebuah struct
.
Gofmt akan melakukannya untuk kita.
Dari deklarasi berikut,
type T struct { name string // name of the object value int // its value }
gofmt
akan mensejajarkan kolom komentar:
type T struct { name string // name of the object value int // its value }
Semua kode Go dalam paket standar telah diformat dengan gofmt
.
Berikut rincian pemformatan, secara ringkas:
- Identasi
-
Kita menggunakan tab untuk identasi. Gunakan spasi hanya bila diperlukan.
- Panjang baris
-
Go tidak memiliki batas panjang baris. Jika baris dirasakan terlalu panjang, potong dan identasikan dengan tab.
- Tanda kurung
-
Go menggunakan lebih sedikit tanda kurung daripada C dan Java: sintaks struktur kontrol (
if
,for
,switch
) tidak menggunakan tanda kurung. Dan juga, hirarki operator lebih singkat dan lebih bersih, sehinggax<<8 + y<<16
makna hirarki operatornya seperti apa yang spasi tandakan, tidak seperti bahasa lainnya.
Pemberian komentar
Go menyediakan blok komentar dengan gaya C //
dan baris komentar gaya C++
//
.
Baris komentar lebih umum digunakan;
blok komentar kebanyakan digunakan untuk mengomentari paket, namun terkadang
berguna juga dalam sebuah ekspresi atau mengindahkan sejumlah blok kode yang
banyak.
Program — dan server web — godoc
memproses sumber kode Go untuk
mengekstraksi dokumentasi dari paket.
Komentar yang ada pada baris deklarasi paling atas, tanpa ada baris kosong,
akan diekstrak bersama dengan deklarasinya sebagai teks yang menjelaskan item
tersebut.
Gaya dan sifat dari komentar tersebut menentukan kualitas dari dokumentasi
yang dihasilkan oleh godoc
.
Setiap paket seharusnya memiliki komentar, atau blok komentar yang mendahului
klausa dari paket tersebut.
Untuk paket dengan banyak berkas, komentar untuk paket hanya perlu
dideklarasikan sekali dalam satu berkas, di berkas manapun.
Komentar tentang paket seharusnya memperkenalkan paket dan menyediakan
informasi yang relevan untuk paket secara keseluruhan.
Ia akan muncul pertama kali dalam halaman godoc
dan diikuti oleh dokumentasi
yang lebih rinci.
/* Package regexp implements a simple library for regular expressions. The syntax of the regular expressions accepted is: regexp: concatenation { '|' concatenation } concatenation: { closure } closure: term [ '*' | '+' | '?' ] term: '^' '$' '.' character '[' [ '^' ] character-ranges ']' '(' regexp ')' */ package regexp
Jika paketnya simpel, komentar dari paket dapat lebih singkat.
// Package path implements utility routines for // manipulating slash-separated filename paths.
Keluaran yang dibangkitkan dari komentar dokumentasi mungkin tidak
direpresentasikan dengan huruf yang baku, jadi jangan bergantung pada
penggunaan spasi untuk melakukan pensejajaran — godoc
, seperti gofmt
,
akan mengurus hal tersebut.
Komentar diinterpretasikan sebagai teks biasa, sehingga HTML dan anotasi
lainnya seperti _ini_ akan menghasilkan keluaran yang sama dan sebaiknya tidak
digunakan.
Salah satu pengaturan yang dimiliki oleh godoc
yaitu menampilkan teks yang
memiliki identasi dengan fonta baku, cocok untuk menampilkan potongan kode.
Komentar
paket fmt
menggunakan cara ini untuk memberikan efek yang bagus.
Bergantung kepada konteks, godoc
bisa saja tidak memformat komentar, jadi
pastikan ia tampak bagus dari awal: gunakan tata tulis dan struktur kalimat
yang benar, potong kalimat yang panjang, dan seterusnya.
Di dalam sebuah paket, komentar apapun setelah deklarasi level paling atas berfungsi sebagai dokumentasi dari deklarasi tersebut. Setiap nama yang diekspor (diawali dengan huruf besar) dalam sumber kode sebaiknya memiliki dokumentasi.
Komentar dokumentasi lebih baik bila dalam satu kalimat lengkap. Kalimat pertama sebaiknya berupa ikhtisar yang dimulai dengan nama yang dideklarasikan.
// Compile parses a regular expression and returns, if successful, // a Regexp that can be used to match against text. func Compile(str string) (*Regexp, error) {
Jika setiap komentar dokumentasi dimulai dengan nama item yang dideskripsikan,
kita dapat menggunakan sub perintah
doc
dari perkakas
go
dan mengambil keluarannya dengan grep
.
Bayangkan misalnya kita lupa nama fungsi "Compile" tapi ingin mencari fungsi
untuk mengurai regular expression, kita tinggal menjalankan perintah
berikut,
$ go doc -all regexp | grep -i parse
Jika semua komentar dokumentasi paket dimulai dengan, "Fungsi ini …", grep
tidak akan dapat membantu kita mencari nama tersebut.
Namun karena setiap komentar dokumentasi paket dimulai dengan nama, kita akan
melihat keluaran seperti berikut,
$ go doc -all regexp | grep -i parse Compile parses a regular expression and returns, if successful, a Regexp MustCompile is like Compile but panics if the expression cannot be parsed. parsed. It simplifies safe initialization of global variables holding $
Sintaks Go membolehkan pengelompokan deklarasi. Sebuah komentar dokumentasi dapat digunakan untuk memperkenalkan sekelompok konstan atau variabel.
// Error codes returned by failures to parse an expression. var ( ErrInternal = errors.New("regexp: internal error") ErrUnmatchedLpar = errors.New("regexp: unmatched '('") ErrUnmatchedRpar = errors.New("regexp: unmatched ')'") ... )
Pengelompokan juga bisa mengindikasikan hubungan antara item, seperti sekelompok variabel yang dilindungi oleh sebuah mutex.
var ( countLock sync.Mutex inputCount uint32 outputCount uint32 errorCount uint32 )
Penamaan
Penamaan dalam Go sama pentingnya seperti pada bahasa lainnya. Mereka bahkan memiliki pengaruh semantik: keterbukaan sebuah nama di luar paket tersebut ditentukan oleh apakah karakter pertamanya menggunakan huruf besar. Oleh karena itu, cukup bermanfaat bila menghabiskan sedikit waktu untuk membaca tentang konvensi penamaan dalam program Go.
Nama paket
Saat sebuah paket diimpor, nama paket menjadi pengakses dari isinya. Setelah
import "bytes"
paket yang mengimpor dapat mengakses bytes.Buffer
.
Sangatlah membantu bila semua penggunaan paket memakai nama yang sama untuk
mengacu pada isinya, hal ini menyiratkan bahwa nama paket haruslah bagus:
singkat, padat, dan evokatif.
Secara konvensi, nama paket menggunakan huruf kecil, dengan satu kata;
tidak perlu garis bawah atau mixedCaps.
Dan jangan khawatir dengan penamaan yang bentrok.
Bila ada nama paket yang bentrok, paket yang mengimpor dapat memilih nama yang
berbeda untuk digunakan secara lokal.
Pada kasus apapun, kesalahan biasanya jarang terjadi karena nama berkas pada
saat impor menentukan paket yang akan digunakan.
Konvensi lainnya yaitu nama paket adalah nama dasar dari direktori sumbernya;
paket dalam src/encoding/base64
diimpor sebagai "encoding/base64"
namun
memiliki nama base64
bukan encoding_base64
dan bukan juga
encodingBase64
.
Pengimpor dari sebuah paket akan menggunakan nama tersebut untuk mengacu pada
isi paket, jadi nama-nama yang diekspor di dalam paket dapat mengacu pada
fakta tersebut untuk mencegah kegagapan.
Misalnya, tipe reader
dengan buffer
dalam paket bufio
disebut dengan
Reader
bukan BufReader
, karena pengguna melihatnya sebagai bufio.Reader
,
yang mana namanya lebih bersih dan singkat.
Lebih lanjut, karena entitas yang diimpor selalu diacu oleh nama paket,
bufio.Reader
tidak bentrok dengan io.Reader
.
Hal yang sama, fungsi yang membuat instansi baru dari ring.Ring
— yang mana
merupakan definisi dari sebuah konstruktor dalam Go — biasanya dipanggil
dengan NewRing
, tapi karena Ring
satu-satunya tipe yang diekspor oleh
paket tersebut, dan karena paket sudah bernamakan ring
, maka ia cukup
dipanggil New
saja, yang oleh pengguna paket dilihat sebagai ring.New
.
Gunakan struktur paket yang baik untuk membantu memilih nama yang bagus.
Salah satu contoh lainnya adalah once.Do
;
once.Do(setup)
lebih mudah dibaca dan tidak lebih baik dari menulis
once.DoOrWaitUntilDone(setup)
.
Nama yang panjang belum tentu membuatnya lebih gampang dibaca.
Sebuah komentar dokumentasi dapat membantu dan lebih bernilai daripada nama
yang panjang.
Getters
Go tidak menyediakan dukungan otomatis untuk fungsi getter dan setter.
Tidak ada yang salah dengan menyediakan fungsi tersebut, terkadang malah lebih
pantas, namun secara idiomatis tidak perlu menambahkan Get
pada nama
getter.
Jika kita memiliki sebuah field bernama owner
(huruf kecil, tidak
diekspor),
method getter sebaiknya dipanggil Owner
(huruf besar, diekspor), bukan
GetOwner
.
Penggunaan huruf besar untuk nama yang diekspor membantu membedakan antara
field dengan method.
Fungsi setter, jika diperlukan, biasanya dipanggil dengan SetOwner
.
Kedua penamaan tersebut secara praktiknya lebih bagus dibaca:
owner := obj.Owner() if owner != user { obj.SetOwner(user) }
Penamaan interface
Secara konvensi, interface dengan satu method dinamakan dengan nama
method plus akhiran -er atau modifikasi yang mirip dengan pembentukan kata
benda bersifat agen: Reader
, Writer
, Formatter
, CloseNotifier
, dll.
Ada sejumlah penamaan seperti itu dan akan lebih produktif bila menghargainya
dan nama fungsi yang dikandungnya.
Read
, Write
, Close
, Flush
, String
dan seterusnya memiliki penanda
dan makna kanonis.
Untuk menghindari kekeliruan, jangan namakan method dengan nama
tersebut kecuali ia memiliki penanda dan makna yang sama.
Sebaliknya, jika tipe mengimplementasikan sebuah method yang memiliki
makna yang sama pada method dengan tipe yang umum, berikan nama dan
penanda yang sama;
beri nama method untuk mengubah sebuah tipe ke string dengan String
bukan
ToString
.
Titik koma (;
)
Seperti bahasa C, gramatika formal dari Go menggunakan titik koma untuk menandakan akhir dari perintah, tapi tidak seperti bahasa C, tanda titik koma tersebut tidak muncul dalam sumber kode. Sebagai gantinya, lexer (program yang membaca sumber kode Go) menggunakan aturan sederhana dengan menambahkan titik koma secara otomatis pada saat memindai sumber kode, sehingga teks input dari sumber kode bebas dari tanda titik koma.
Aturannya adalah sebagai berikut.
Jika token terakhir sebelum baris baru adalah sebuah pengidentifikasi
(termasuk kata seperti int
dan float64
), atau token harfiah seperti angka
atau konstan string, atau salah satu dari token berikut
break continue fallthrough return ++ -- ) }
maka lexer akan menambahkan titik koma setelah token. Hal ini bisa disimpulkan menjadi, "jika baris baru muncul setelah sebuah token yang dapat mengakhiri sebuah perintah, tambahkan titik koma".
Sebuah titik koma juga dapat diindahkan sebelum tanda kurung tutup kurawal, sehingga perintah seperti berikut
go func() { for { dst <- <-src } }()
tidak memerlukan titik koma.
Program Go yang idiomatis hanya memerlukan titik koma pada klausa pengulangan
for
, untuk memisahkan antara elemen inisialisasi, kondisi, dan kontinuasi.
Salah satu konsekuensi dari aturan penambahan titik koma ini adalah kita tidak
bisa menempatkan kurung kurawal buka dari struktur kontrol (if
, for
,
switch
, atau select
) pada baris selanjutnya.
Jika melakukan hal ini, sebuah titik koma akan disisipkan sebelum tanda
kurung kurawal, yang mana bisa menyebabkan efek yang tidak diiinginkan.
Tulislah kode tersebut dengan cara seperti berikut
if i < f() { g() }
bukan seperti ini
if i < f() // salah! { // salah! g() }
Struktur kontrol
Struktur kontrol dari Go berkaitan dengan bahasa C namun berbeda dalam
hal-hal tertentu.
Tidak ada pengulangan do
atau while
, hanya for
;
switch
yang lebih fleksibel;
if
dan switch
bisa menggunakan perintah inisialisasi seperti halnya pada
for
;
perintah break
dan continue
memiliki label identifikasi yang opsional;
dan ada beberapa kontrol struktur baru termasuk switch
pada tipe dan
komunikasi multiplexer, select
.
Sintaksnya juga sedikit berbeda: tidak ada tanda kurung dan bagian badan dari
kontrol harus selalu dibatasi oleh kurung kurawal.
If
Dalam Go if
yang sederhana itu bentuknya seperti ini:
if x > 0 { return y }
Wajibnya kurung kurawal mendorong penulisan perintah if
menjadi beberapa
baris.
Gaya penulisan seperti ini sangat bagus, khususnya bila bagian badan kondisi
memiliki perintah kontrol seperti return
atau break
.
Secara if
dan switch
dapat melakukan perintah inisialisasi, maka sangat
umum melihatnya digunakan untuk mendeklarasikan lokal variabel.
if err := file.Chmod(0664); err != nil { log.Print(err) return err }
Dalam pustaka Go, akan ditemukan bila perintah if
tidak mengalir ke perintah
selanjutnya—yakni, badan dari kondisi berakhir dengan break
,
continue
, goto
, atau return
—kondisi else
yang tidak berguna
dihilangkan.
f, err := os.Open(name) if err != nil { return err } codeUsing(f)
Ini adalah sebuah contoh situasi umum yang mana kode harus menjaga seurutan
kondisi eror.
Kode akan mudah dibaca jika kontrol yang sukses terus mengalir ke bawah,
mengeliminasi kasus-kasus yang eror saat mereka muncul.
Karena kasus yang eror condong berakhir dengan perintah return
, maka kode
tidak memerlukan perintah 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 dan penempatan ulang
Contoh terakhir dari bagian sebelumnya memperlihatkan rincian dari bagaimana
bentuk deklarasi singkat :=
bekerja.
Deklarasi yang memanggil os.Open
berbunyi,
f, err := os.Open(name)
Perintah tersebut mendeklarasikan dua variabel, f
dan err
.
Beberapa baris selanjutnya, panggilan ke f.Stat
dibaca,
d, err := f.Stat()
yang tampak seperti ia mendeklarasikan d
dan err
.
Perhatikan, err
muncul dikedua perintah.
Duplikasi seperti ini legal: err
dideklarasikan oleh perintah pertama, tapi
digunakan kembali pada perintah kedua.
Ini artinya panggilan ke f.Stat
menggunakan variabel err
yang sama yang
dideklarasikan di atas, dan diberikan nilai yang baru.
Dalam deklarasi sebuah variabel v
dengan ":=" , variabel tersebut akan
digunakan ulang walaupun telah dideklarasikan, selama:
-
deklarasi tersebut berada dalam skop yang sama dengan deklarasi sebelumnya dari
v
(jikav
dideklarasikan di skop luarnya, deklarasi akan membuat variabel yang baru §), -
nilai dalam inisialiasi dapat ditempatkan ke
v
, dan -
setidaknya ada satu variabel lain, yang baru, dalam deklarasi
Properti yang tidak biasa seperti ini murni karena pragmatisme, membuat kita
lebih mudah menggunakan nilai tunggal err
, sebagai contohnya, dalam beberapa
if-else
.
Anda akan sering melihat penggunaan seperti ini.
§ Perlu diingat di sini, bahwa dalam Go, skop dari parameter fungsi dan nilai kembalian sama dengan badan fungsi, walaupun ia tampak secara leksikal berada di luar kurung kurawal yang menutup badan fungsi.
For
Pengulangan for
pada Go mirip—tapi tidak sama—dengan C.
Ia menggabungkan for
dan while
dan tidak ada for-while
.
Ada tiga bentuk pengulangan for
, hanya satu yang menggunakan titik koma.
// Seperti "for" pada C for inisialisasi; kondisi; selanjutnya { } // Seperti "while" pada C for kondisi { } // Seperti "for(;;)" pada C for { }
Deklarasi singkat membuatnya mudah mendeklarasikan variabel index di dalam pengulangan.
sum := 0 for i := 0; i < 10; i++ { sum += i }
Jika melakukan pengulangan pada array, slice, string, atau map, atau
membaca dari sebuah channel, sebuah klausa range
dapat digunakan pada
pengulangan.
for key, value := range oldMap { newMap[key] = value }
Jika hanya membutuhkan item pertama dalam range
(key dari map atau
indeks dari array/slice/string), hapus variabel kembalian kedua,
for key := range m { if key.expired() { delete(m, key) } }
Jika hanya membutuhkan item kedua (nilainya), gunakan pengidentifikasi kosong, sebuah garis bawah, untuk mengindahkan yang pertama:
sum := 0 for _, value := range array { sum += value }
Pengidentifikasi kosong memiliki banyak kegunaan, seperti yang akan dijelaskan di bagian nanti.
Untuk string, range
bekerja lebih, memecah setiap kode poin dari Unicode
dengan mengurai UTF-8.
Pengkodean yang salah mengkonsumsi satu byte dan menghasilkan rune
pengganti U+FFFD.
(Nama rune adalah terminologi Go untuk sebuah kode poin Unicode tunggal.
Lihat spesifikasi bahasa untuk lebih detilnya.)
Pengulangan
for pos, char := range "日本\x80語" { // \x80 adalah sebuah pengkodean UTF-8 yang ilegal fmt.Printf("karakter %#U dimulai pada posisi byte ke %d\n", char, pos) }
mencetak
karakter U+65E5 '日' dimulai pada posisi byte ke 0 karakter U+672C '本' dimulai pada posisi byte ke 3 karakter U+FFFD '�' dimulai pada posisi byte ke 6 karakter U+8A9E '語' dimulai pada posisi byte ke 7
Terakhir, Go tidak memiliki operator koma dan perintah ` dan `--` bukanlah
sebuah ekspresi.
Maka, jika ingin menggunakan beberapa variabel dalam sebuah `for`, kita
harus menggunakan penempatan paralel (cara ini tidak membolehkan `
dan
—
).
// Reverse 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
Kontrol switch
pada Go lebih generik daripada C.
Ekspresi switch
pada Go tidak harus konstan maupun integer, bagian kondisi
case
dievaluasi dari atas ke bawah sampai ditemukan kondisi yang sesuai, dan
jika ekspresi switch
tidak memiliki ekspresi, ia akan memeriksa kondisi
case
yang bernilai true
.
Oleh karena itu, memungkinkan—dan idiomatis—untuk menulis kondisi
if-else-if-else
menggunakan 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 seperti C, case
pada Go tidak otomatis jatuh ke bawah, namun
kondisi case
bisa lebih dari satu yang dipisahkan dengan koma,
func shouldEscape(c byte) bool { switch c { case ' ', '?', '&', '=', '#', '+', '%': return true } return false }
Perintah break
pada Go bisa digunakan untuk mengakhiri blok switch
.
Terkadang, perlu juga untuk keluar dari pengulangan, namun bukan dari
switch
, dan dalam Go hal ini bisa dilakukan dengan memberi label pada
pengulangan dan "keluar" dari label tersebut.
Contoh berikut memperlihatkan penggunaan kedua break
tersebut.
Loop: for n := 0; n < len(src); n += size { switch { case src[n] < sizeOne: if validateOnly { break // keluar dari switch, tapi tetap dalam pengulangan `for` } size = 1 update(src[n]) case src[n] < sizeTwo: if n+1 >= len(src) { err = errShortInput break Loop // keluar dari `switch` dan pengulangan `for` } if validateOnly { break // keluar dari switch, tapi tetap dalam pengulangan `for` } size = 2 update(src[n] + src[n+1]<<shift) } }
Tentu saja, perintah continue
juga dapat menggunakan label tapi hanya
berlaku pada pengulangan.
Untuk mengakhiri bagian ini, berikut fungsi yang membandingkan dua slice byte
menggunakan dua perintah switch
:
// Compare mengembalikan sebuah integer hasil pembandingan dari dua slice byte // secara leksikografi. // Hasilnya adalah 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 }
Switch dengan tipe
Perintah switch
juga bisa digunakan untuk menemukan tipe dinamis dari sebuah
variabel interface.
Tipe switch
seperti ini menggunakan sintaks dengan kata kunci type
dalam
tanda kurung.
Jika perintah switch
mendeklarasikan sebuah variabel dalam ekspresinya,
variabel tersebut akan memiliki tipe korespondensinya di setiap klausa.
Termasuk idiomatis menggunakan nama yang sama dalam kasus ini, efeknya
mendeklarasikan variabel baru dengan nama yang sama tapi dengan tipe yang
berbeda di setiap case
.
var t interface{} t = functionOfSomeType() switch t := t.(type) { default: fmt.Printf("unexpected type %T\n", t) // %T mencetak tipe dari `t` case bool: fmt.Printf("boolean %t\n", t) // t bertipe bool case int: fmt.Printf("integer %d\n", t) // t bertipe int case *bool: fmt.Printf("pointer to boolean %t\n", *t) // t bertipe *bool case *int: fmt.Printf("pointer to integer %d\n", *t) // t bertipe *int }
Fungsi
Nilai kembalian multipel
Salah satu fitur tidak umum dari Go yaitu fungsi dan method dapat mengembalikan lebih dari satu nilai. Bentuk ini dapat digunakan untuk memperkaya beberapa idiom ceroboh dalam program C: mengembalikan nilai eror, seperti -1 atau EOF, dan mengubah argumen yang dikirim lewat alamat dari variabel (atau dikenal juga dengan pointer).
Pada C, eror dari fungsi write
diketahui dari nilai negatif yang
dikembalikan dengan kode eror tersimpan terpisah dalam sebuah lokasi yang
volatile.
Pada Go, Write
dapat mengembalikan jumlah tertulis dan eror: "Kita berhasil
menulis sejumlah byte tapi tidak semuanya karena perangkat telah penuh".
Bentuk method Write
terhadap berkas dalam paket os
yaitu:
func (file *File) Write(b []byte) (n int, err error)
dan seperti yang disebutkan dalam dokumentasi, fungsi tersebut mengembalikan
jumlah byte tertulis dan nilai eror non nil
bila n != len(b)
.
Bentuk seperti ini sangat umum pada Go;
lihat bagian penanganan eror untuk melihat contoh lebih banyak.
Pendekatan ini juga menghilangkan kebutuhan untuk mengirim pointer sebagai nilai kembalian. Berikut sebuah fungsi sederhana untuk memindai sebuah angka dari slice byte, yang mengembalikan angka yang terpindai dan posisi selanjutnya.
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 menggunakan fungsi tersebut untuk memindai angka dari sebuah input
slice b
seperti berikut:
for i := 0; i < len(b); { x, i = nextInt(b, i) fmt.Println(x) }
Parameter kembalian bernama
Parameter kembalian atau hasil dari sebuah fungsi dapat diberi nama dan
dipakai seperti variabel, seperti halnya paramater pada fungsi.
Bila diberi nama, ia diinisialisasi dengan nilai kosong dari tipenya saat
fungsi dimulai;
jika fungsi mengeksekusi perintah return
tanpa argumen, nilai terakhir dari
parameter kembalian digunakan sebagai nilai kembalian.
Pemberian nama ini bukanlah sebuah keharusan namun dapat membuat kode lebih
singkat dan jelas: nama kembalian sebagai dokumentasi.
Jika kita beri nama kembalian pada fungsi nextInt
maka akan memperjelas
makna nilai int
yang dikembalikan.
func nextInt(b []byte, pos int) (value, nextPos int) {
Karena kembalian bernama diinisialisasi dan terikat dengan perintah return
,
cara ini dapat digunakan untuk mempermudah sebagaimana juga memperjelas kode.
Berikut versi dari io.ReadFull
yang menggunakan cara ini dengan bagus:
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 }
Penangguhan (defer)
Perintah defer
pada Go menangguhkan pemanggilan sebuah fungsi sampai fungsi
yang mengeksekusi defer
berakhir.
Cara ini efektif untuk situasi yang harus menghapus alokasi atau sumber daya.
Contoh umum dari penggunaan defer
adalah membuka mutex atau menutup
berkas.
// Contents mengembalikan isi dari berkas sebagai string. func Contents(filename string) (string, error) { f, err := os.Open(filename) if err != nil { return "", err } defer f.Close() // f.Close akan dijalankan saat fungsi selesai. var result []byte buf := make([]byte, 100) for { n, err := f.Read(buf[0:]) result = append(result, buf[0:n]...) // append is discussed later. if err != nil { if err == io.EOF { break } return "", err // f akan ditutup saat fungsi berakhir di sini. } } return string(result), nil // f akan ditutup saat fungsi berakhir di sini. }
Menangguhkan pemanggilan sebuah fungsi seperti Close
memberikan dua
kelebihan.
Pertama, akan menjamin bahwa berkas tidak lupa ditutup kembali, sebuah
kesalahan yang mudah terjadi jika kita mengubah fungsi untuk menambah sebuah
perintah return
di bagian manapun.
Kedua, penutupan berkas berada dekat dengan pembukaan berkas, yang membuat
kode lebih jelas daripada menempatkan penutupan di akhir fungsi.
Argumen dari fungsi atau method yang ditangguhkan dievaluasi saat perintah
defer
dieksekusi, bukan saat fungsi atau method dieksekusi.
Hal ini berarti sebuah perintah defer
dapat dieksekusi berulang kali.
Berikut contoh sederhana,
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }
Fungsi yang ditangguhkan dieksekusi secara LIFO (Last In First Out—Terakhir
Masuk Pertama Keluar), sehingga kode di atas akan mencetak 4 3 2 1 0
saat
fungsi berakhir.
Contoh penggunaan defer
yang lain yaitu melacak eksekusi fungsi, seperti
berikut:
func trace(s string) { fmt.Println("masuk:", s) } func untrace(s string) { fmt.Println("keluar:", s) } // Gunakan seperti berikut: func a() { trace("a") defer untrace("a") // do something.... }
Karena argumen dari fungsi yang di- defer
dievaluasi saat defer
dieksekusi, kita dapat mengeksploitasinya lebih lanjut.
Fungsi trace
dapat mengatur argumen dari fungsi untrace
.
Contohnya:
func trace(s string) string { fmt.Println("masuk:", s) return s } func un(s string) { fmt.Println("keluar:", s) } func a() { defer un(trace("a")) fmt.Println("in a") } func b() { defer un(trace("b")) fmt.Println("in b") a() } func main() { b() }
mencetak
entering: b in b entering: a in a leaving: a leaving: b
Bagi pemrogram yang terbiasa dengan manajemen sumber daya dalam bentuk blok
kode dari bahasa pemrograman lain, defer
mungkin tampak aneh, namun
penerapannya datang dari fakta bahwa ia bukan berbasis-blok tapi
berbasis-fungsi.
Dalam bagian panic
dan recover
kita akan melihat contoh lain dari
kemampuan defer
.
Data
Alokasi dengan new
Go memiliki dua fungsi bawaan, primitif untuk alokasi: new
dan make
.
Kedua fungsi tersebut melakukan hal yang berbeda dan berlaku pada tipe yang
berbeda, yang bisa membingungkan, tapi aturannya cukup sederhana.
Mari kita telaah new
terlebih dahulu.
Fungsi new
digunakan untuk mengalokasikan memory, tapi tidak seperti
bahasa lain, ia tidak menginisialisasi memory, hanya mengosongkan saja.
new(T)
mengalokasikan penyimpanan kosong untuk item bertipe T
dan
mengembalikan alamatnya, sebuah nilai bertipe *T
.
Dalam terminologi Go, fungsi new
mengembalikan sebuah pointer dari nilai
kosong hasil alokasi dari tipe T
.
Secara memory yang dikembalikan oleh new
dikosongkan, maka sangatlah
membantu merancang struktur data sehingga setiap nilai kosong dari tipe
dapat digunakan tanpa perlu diinisialisasi.
Hal ini berarti pengguna dari struktur data dapat membuatnya dengan new
dan
langsung menggunakannya.
Sebagai contohnya, dokumentasi untuk bytes.Buffer
berbunyi "nilai kosong
dari Buffer
adalah sebuah buffer kosong yang siap digunakan."
Hal yang sama, sync.Mutex
tidak memiliki eksplisit konstruktor atau method
Init
.
Namun, nilai kosong dari sync.Mutex
didefinisikan sebagai mutex yang tidak
terkunci.
Properti dari nilai-kosong bekerja secara transitif. Perhatikan deklarasi tipe berikut.
type SyncedBuffer struct { lock sync.Mutex buffer bytes.Buffer }
Nilai dari tipe SyncedBuffer
siap digunakan langsung setelah alokasi atau
deklarasi.
Pada contoh kode selanjutnya, p
dan v
dapat digunakan tanpa memerlukan
pengaturan lebih lanjut.
p := new(SyncedBuffer) // tipe *SyncedBuffer var v SyncedBuffer // tipe SyncedBuffer
Konstruktor dan inisialisasi komposit
Terkadang nilai kosong tidak cukup dan inisialisasi dengan konstruktor
diperlukan, seperti contoh berikut yang diambil dari paket os
.
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 }
Kode di atas dapat disederhanakan menggunakan inisialisasi komposit, yaitu sebuah ekspresi yang membuat instansi baru dari tipe komposit setiap kali dievaluasi.
func NewFile(fd int, name string) *File { if fd < 0 { return nil } f := File{fd, name, nil, 0} return &f }
Tidak seperti C, dalam Go kita dapat mengembalikan alamat dari variabel lokal; alokasi dari variabel bertahan setelah fungsi berakhir. Malah, mengambil alamat dari inisialisasi komposit akan mengalokasi instansi yang baru setiap kali dievaluasi, sehingga kita dapat menggabungkan dua baris terakhir.
return &File{fd, name, nil, 0}
Field dari inisialisasi komposit diset berurutan dan semuanya harus ada.
Namun, dengan memberi label pada setiap elemen secara eksplisit dengan cara
field:nilai
, inisialisasi dapat dibentuk tanpa harus berurutan, dengan
field yang tidak dicantumkan akan diberi nilai kosong.
Sehingga kita dapat tulis
return &File{fd: fd, name: name}
Jika inisialisasi komposit tidak memiliki field apapun, ia akan otomatis
diinisialisasi dengan nilai kosong dari tipenya.
Ekspresi new(File)
dan &File{}
adalah sama.
Inisialisasi komposit juga berlaku pada array, slice, dan map, dengan label
pada field adalah indeks atau key dari map
.
Pada contoh berikut, inisialisasi bekerja sesuai dengan nilai Enone
, Eio
,
dan Einval
, selama nilainya 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)
memiliki fungsi yang berbeda dari new(T)
.
Ia hanya dapat digunakan pada slice, map
, dan channel
, yang
mengembalikan inisialisasi (bukan nilai kosong) dari tipe T
(bukan *T
).
Alasan dari perbedaan ini adalah ketiga tipe tersebut merepresentasikan
referensi ke struktur data yang harus diinisialisasi sebelum digunakan.
Sebuah slice, sebagai contohnya, adalah sebuah struktur data yang memiliki
tiga field internal (yang tidak diekspor), yang terdiri dari sebuah
pointer ke data (sebuah array), panjang, dan kapasitas;
dan sebelum item-item tersebut diinisialisasi, maka slice bernilai nil
.
Untuk slice, map
, dan channel
, make
menginisialisasi struktur data
internal mereka dan menyiapkan nilainya.
Misalnya,
make([]int, 10, 100)
mengalokasikan sebuah array 100 buah int
dan membuat sebuah struktur
slice dengan panjang 10 dan kapasitas 100, yang menunjuk ke elemen pertama
dari 10 elemen dari array.
(Saat membuat slice, kapasitasnya bisa diindahkan; lihat bagian tentang
slice untuk informasi lebih lanjut.)
Kebalikannya, new([]int)
mengembalikan sebuah pointer ke struktur slice
yang baru dialokasikan dan dikosongkan, yaitu, sebuah pointer ke sebuah
slice bernilai nil
.
Contoh berikut mengilustrasikan perbedaan antara new
dan make
.
var p *[]int = new([]int) // alokasi struktur slice; *p == nil; jarang digunakan var v []int = make([]int, 100) // slice v sekarang mengacu ke array dari 100 int // Lebih kompleks lagi: var p *[]int = new([]int) *p = make([]int, 100, 100) // Idiomatis: v := make([]int, 100)
Ingatlah bahwa make
hanya berlaku untuk map
, slice, dan channel
dan
tidak mengembalikan sebuah pointer.
Untuk mendapatkan pointer, alokasikan dengan new
atau ambil alamat dari
variabel secara langsung.
Array
Array berguna saat merancang susunan memory yang lebih rinci dan terkadang membantu menghindari alokasi memory, tapi pada umumnya ia adalah elemen pembentuk dari slice, yang akan kita bahas di bagian berikutnya. Untuk membantu memahami bagian selanjutnya, berikut beberapa penjelasan tentang array.
Ada tiga perbedaan utama untuk array antara Go dan C. Dalam Go,
-
Array adalah nilai. Memberikan sebuah array ke array lainnya menyalin semua elemennya.
-
Jika mengirim sebuah array ke sebuah fungsi, fungsi akan menerima salinan dari array, bukan pointer dari array.
-
Ukuran dari array adalah bagian dari tipe. Tipe dari
[10]int
dan[20]int
adalah berbeda.
Properti "array adalah nilai" terkadang bisa berguna namun cukup memakan sumber daya; jika ingin perilaku dan efisiensi seperti C, kita bisa mengirim 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) // Note the explicit address-of operator
Namun gaya kode seperti ini (pointer ke sebuah array pada argumen dari fungsi) tidak idiomatis pada Go. Lebih baik gunakan slice.
Slice
Slice membungkus array untuk memberikan sebuah antar muka yang lebih umum, kuat, dan mudah dari data yang berurutan. Kecuali untuk item-item dengan dimensi yang jelas seperti matriks, umumnya pemrograman array dalam Go menggunakan slice bukan array.
Slice menyimpan referensi dari array yang menjadi dasarnya, dan jika
menempatkan sebuah slice ke slice yang lainnya, keduanya akan mengacu pada
array yang sama.
Jika sebuah fungsi menerima argumen berupa slice, perubahan yang dilakukan
terhadap elemen dari slice akan dapat dilihat oleh yang memanggil fungsi,
analoginya sama dengan mengirim pointer ke array di dalamnya.
Oleh karena itu, sebuah fungsi Read
dapat menerima argumen slice bukan
pointer dan sebuah hitungan;
panjang dari slice menentukan batas atas dari berapa banyak data yang akan
dibaca.
Berikut method Read
dari tipe File
dalam packet os
:
func (f *File) Read(buf []byte) (n int, err error)
Method tersebut mengembalikan jumlah byte yang dibaca dan sebuah nilai
eror.
Untuk membaca 32 byte pertama ke dalam sebuah buffer buf
, potong
buffer tersebut.
n, err := f.Read(buf[0:32])
Pemotongan seperti di atas umum dan efisien. Bahkan, lupakan efisiensi untuk sementara, potongan kode berikut juga membaca 32 byte pertama ke dalam buffer.
var n int var err error for i := 0; i < 32; i++ { nbytes, e := f.Read(buf[i:i+1]) // Read one byte. n += nbytes if nbytes == 0 || e != nil { err = e break } }
Panjang dari sebuah slice bisa diubah selama ia masih selaras dengan limit
dari array yang dikandungnya.
Kapasitas dari slice, dapat diakses dengan fungsi bawaan cap
, menandakan
panjang maksimum dari slice.
Berikut fungsi untuk menambahkan data ke sebuah slice.
Jika data melebihi kapasitas, maka slice tersebut akan direalokasikan.
Dan hasil dari alokasi baru akan dikembalikan.
Fungsi berikut memberitahu bahwa len
dan cap
bisa digunakan pada slice
yang nil
, dan nilai kembaliannya adalah 0
.
func Append(slice, data []byte) []byte { l := len(slice) if l + len(data) > cap(slice) { // reallocate // Gandakan alokasi dari yang dibutuhan, untuk pertumbuhan // kedepannya. newSlice := make([]byte, (l+len(data))*2) // Fungsi `copy` adalah bawaan dan dapat digunakan untuk slice // bertipe apapun. copy(newSlice, slice) slice = newSlice } slice = slice[0:l+len(data)] copy(slice[l:], data) return slice }
Kita harus mengembalikan slice sesudahnya karena, walaupun Append
dapat
memodifikasi elemen dari slice, slice itu sendiri (struktur data yang
menyimpan pointer, panjang, dan kapasitas) dikirim secara nilai.
Fungsi penambahan terhadap slice ini sangat berguna, ia juga dapat digantikan
dengan fungsi bawaan append
.
Untuk memahami rancangan fungsi di atas, kita membutuhkan sedikit informasi
tambahan, jadi kita akan kembali ke fungsi tersebut nanti.
Slice dua dimensi
Array dan slice pada Go berdimensi tunggal. Untuk membuat array atau slice dua dimensi (2D), definisikan array-dari-array atau slice-dari-slice, seperti berikut:
type Transform [3][3]float64 // Array 3x3 bertipe float64 type LinesOfText [][]byte // Slice dari slice bertipe byte.
Karena slice memiliki variabel panjang, maka memungkinkan setiap bagian dalam
slice memiliki panjang yang berbeda.
Situasi tersebut sangat umum, seperti pada contoh LinesOfText
: setiap baris
memiliki panjangnya sendiri-sendiri.
text := LinesOfText{ []byte("Now is the time"), []byte("for all good gophers"), []byte("to bring some fun to the party."), }
Terkadang perlu juga mengalokasikan slice 2D, situasi seperti ini bisa digunakan saat memindai barisan pixel, misalnya. Ada dua cara untuk menyelesaikan masalah ini. Salah satunya yaitu dengan mengalokasikan setiap slice tersendiri; cara lainnya yaitu dengan mengalokasikan sebuah array yang menunjuk ke setiap slice. Yang mana yang mau digunakan bergantung kepada aplikasi yang dibuat. Jika slice bisa bertambah atau berkurang, maka mereka harus dialokasikan tersendiri untuk menghindari menimpa baris selanjutnya; jika tidak, akan lebih efisien mengkonstruksi objek dengan alokasi tunggal. Sebagai referensi, berikut bentuk dari kedua cara tersebut. Pertama, dengan cara alokasi tersendiri,
// Alokasikan slice paling atas. picture := make([][]uint8, YSize) // Satu baris per unit dari y. // Lakukan pengulangan ke setiap baris, alokasikan slice untuk setiap baris. for i := range picture { picture[i] = make([]uint8, XSize) }
Dan cara dengan alokasi tunggal, dipotong menjadi baris,
// Alokasikan slice paling atas, seperti sebelumnya. picture := make([][]uint8, YSize) // One row per unit of y. // Alokasikan sejumlah besar slice untuk menampung semua pixel. pixels := make([]uint8, XSize*YSize) // Bertipe []uint8 walaupun picture adalah [][]uint8. // Lakukan pengulangan ke setiap baris, memotong setiap baris dari depan slice // pixel yang tersisa. for i := range picture { picture[i], pixels = pixels[:XSize], pixels[XSize:] }
Map
Map adalah struktur data bawaan yang mudah dan kuat yang mengasosiasikan
sebuah tipe (kunci) dengan nilai tipe lainnya (elemen atau nilai).
Kunci dari map bisa bertipe apa saja selama operator ekualitas (==
) berlaku,
seperti integer, float dan bilangan komples, string, pointer, interface
(selama tipe dinamis mendukung ekualitas), struct
dan array
.
Slice tidak bisa dipakai sebagai kunci dari map, karena sifat ekualitas tidak
terdefinisi pada slice.
Seperti slice, map menyimpan referensi ke struktur data di dalamnya.
Jika mengirim sebuah map ke sebuah fungsi yang mengubah isi dari map,
perubahan tersebut akan dapat dilihat oleh yang memanggil fungsi.
Map dapat dibuat menggunakan sintaks inisialisasi komposit dengan pasangan kunci-nilai yang dipisahkan oleh titik dua, sehingga sangat mudah membuat map dengan 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, }
Menset dan mengambil nilai map secara sintaks mirip dengan array dan slice kecuali indeksnya tidak harus sebuah integer.
offset := timeZone["EST"]
Bila kunci dari map tidak ada, ia akan mengembalikan nilai kosong dari tipe
nilai dari map.
Misalnya, jika map berisi integer, mengambil nilai dari kunci yang tidak ada
akan mengembalikan 0
.
Sebuah pengelompokan dapat diimplementasikan dengan map yang nilainya bertipe
bool
.
Set nilai map dengan true
untuk menyimpan sebuah nilai ke dalam kelompok,
dan kemudian periksa dengan pengindeksan biasa.
attended := map[string]bool{ "Ann": true, "Joe": true, ... } if attended[person] { // akan bernilai false jika person tidak ada dalam map fmt.Println(person, "sedang rapat") }
Terkadang perlu membedakan antara nilai yang tidak ada dengan nilai kosong.
Apakah ada isi untuk "UTC" atau ia 0
karena tidak ada di dalam map sama
sekali?
Anda bisa membedakan ini dengan cara kembalian multipel.
var seconds int var ok bool seconds, ok = timeZone[tz]
Untuk alasan khusus cara ini disebut dengan idiom "comma ok".
Pada contoh berikut, jika tz
ada, nilai seconds
akan di set dan ok
akan
bernilai true
;
jika tidak, seconds
akan di set dengan 0
dan ok
akan bernilai false
.
Berikut fungsi yang mengimplementasikannya dengan laporan kesalahan:
func offset(tz string) int { if seconds, ok := timeZone[tz]; ok { return seconds } log.Println("zona waktu tidak dikenal:", tz) return 0 }
Untuk menguji keberadaan sebuah nilai dalam map tanpa perlu tahu nilai
sebenarnya, kita bisa menggunakan identifikasi kosong (_
) di variabel tempat
nilai disimpan.
_, present := timeZone[tz]
Untuk menghapus sebuah isi map, gunakan fungsi bawaan delete
, yang
argumennya adalah map dan kunci yang akan dihapus.
Cara ini aman bahkan bila kunci tidak ada dalam map.
delete(timeZone, "PDT") // Now on Standard Time
Pencetakan
Pencetakan berformat dalam Go menggunakan gaya yang mirip dengan printf
pada
C tapi lebih kaya dengan format dan lebih generik.
Fungsi pencetakan ada dalam paket fmt
: fmt.Printf
, fmt.Fprintf
,
fmt.Sprintf
, dan seterusnya.
Fungsi string (Sprintf
dll.) mengembalikan sebuah string bukan mengisi
buffer pada argumen.
Anda tidak perlu menyediakan string yang berformat.
Untuk setiap Printf
, Fprintf
, dan Sprintf
ada pasangan fungsi lain,
yaitu Print
dan Println
.
Fungsi tersebut tidak menerima string berformat melainkan membangkitkan format
standar untuk setiap argumennya.
Versi dari Println
menyisipkan sebuah spasi antara argumen dan menambahkan
baris baru di akhir, sementara versi Print
hanya menyisipkan spasi jika
argumen dikedua sisi adalah string.
Dalam contoh berikut setiap baris perintah menghasilkan keluaran 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 berformat fmt.Fprint
dan teman-temannya menerima objek apapun yang
mengimplementasikan interface
io.Writer
;
Contoh yang paling awam yaitu variabel os.Stdout
dan os.Stderr
.
Dari sini kita mulai menyimpang dari C.
Pertama, format numerik seperti %d
tidak menerima tanda ukuran atau tanda
signed atau unsigned;
namun, fungsi dari pencetakan akan menggunakan tipe dari argumen untuk
menentukan properti tersebut.
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 hanya menginginkan konversi standar, seperti desimal untuk integer,
kita bisa menggunakan format keseluruhan %v
(untuk "value");
hasilnya persis seperti yang Print
dan Println
akan keluarkan.
Lebih lanjut, format tersebut dapat mencetak nilai apapun, bahkan array,
slice, struct, dan map.
Berikut perintah pencetakan untuk map zona waktu yang didefinisikan pada
bagian sebelumnya.
fmt.Printf("%v\n", timeZone) // atau bisa fmt.Println(timeZone)
yang mengeluarkan
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
Untuk map, kuncinya akan dicetak tidak berurutan.
Pada struct
, format %+v
mencetak nama field, dan untuk nilai apapun
format %#v
akan mencetak nilai dengan sintaks Go.
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, "PST":-28800, "EST":-18000, "UTC":0, "MST":-25200}
(Perhatikan tanda "&".)
Format dengan tanda kutip ganda juga tersedia lewat %q
saat digunakan pada
tipe string atau []byte
.
Format %#q
akan menggunakan kutip terbalik "” jika memungkinkan.
(Format %q
juga berlaku untuk integer dan rune, menghasilkan konstanta
rune
dengan tanda kutip tunggal.)
Selain itu, format %x
dapat digunakan juga untuk string, array dari byte,
dan slice dari byte, sebagaimana juga integer, menghasilkan string dengan
heksadesimal, dan bila sebuah spasi ditambahkan dalam format (% x
) ia akan
mencetak spasi antara byte.
Format berguna lainnya yaitu %T
, yang mencetak tipe dari sebuah nilai.
fmt.Printf("%T\n", timeZone)
mencetak
map[string]int
Jika ingin mengontrol format untuk sebuah tipe kostum, cukup dengan
mendefinisikan sebuah method String() string
pada tipe tersebut.
Untuk tipe sederhana type T
berikut, bentuknya seperti ini.
func (t *T) String() string { return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c) } fmt.Printf("%v\n", t)
yang mencetak dengan format
7/-2.35/"abc\tdef"
Jika ingin mencetak nilai dari tipe T
atau pointer ke T
, maka
receiver dari String
haruslah nilai dari tipe;
contoh di atas menggunakan pointer karena ia lebih efisien dan idiomatis
untuk tipe dengan struct
.
Lihat bagian di bawah tentang
pointer dan nilai
untuk informasi lebih lanjut.
Method String
bisa memanggil Sprintf
karena fungsi pencetakan secara
penuh berdiri sendiri dan bisa dibungkus dengan cara tersebut.
Ada hal penting yang harus dipahami mengenai pendekatan ini: jangan membuat
method String
dengan memanggil Sprintf
yang mana akan mengulang method
String
terus menerus.
Hal ini bisa terjadi jika pemanggilan Sprintf
mencoba mencetak langsung
receiver sebagai sebuah string, yang mana akan memanggil method itu
kembali.
Hal ini umum dan kesalahan yang mudah terjadi, seperti yang diperlihatkan pada
contoh berikut.
type MyString string func (m MyString) String() string { return fmt.Sprintf("MyString=%s", m) // Eror: akan berulang selamanya. }
Perbaikannya cukup mudah: konversi argumen ke tipe dasar string, yang tidak
memiliki method String
.
type MyString string func (m MyString) String() string { return fmt.Sprintf("MyString=%s", string(m)) // OK: note conversion. }
Pada bagian inisialisasi kita akan melihat teknik lain untuk menghindari rekursi ini.
Teknik pencetakan lainnya yaitu dengan mengirim argumen dari fungsi pencetakan
langsung ke fungsi lain.
Fungsi Printf
menggunakan tipe …interface{}
pada argumen terakhirnya
untuk dapat menerima beragam jumlah parameter (dari berbagai tipe) yang dapat
muncul setelah format.
func Printf(format string, v ...interface{}) (n int, err error) {
Dalam fungsi Printf
, v
berlaku seperti sebuah varibel bertipe
[]interface{}
namun bila dikirim ke fungsi variadic lainnya, ia bersifat
sama seperti daftar argumen biasa.
Berikut implementasi dari fungsi log.Println
yang kita gunakan di atas.
Ia mengirim argumennya langsung ke fmt.Sprintln
untuk melakukan pemformatan.
// Println prints to the standard logger in the manner of fmt.Println. func Println(v ...interface{}) { std.Output(2, fmt.Sprintln(v...)) // Output takes parameters (int, string) }
Kita menulis …
setelah v
pada pemanggilan ke Sprintln
untuk memberi
tahu compiler memperlakukan v
sebagai daftar argumen;
kalau tidak ia akan mengirim v
sebagai sebuah slice.
Ada lebih banyak lagi mengenai pencetakan dari yang sudah kita bahas di sini.
Lihat dokumentasi godoc
pada paket fmt
untuk lebih detilnya.
Sebuah parameter …
bisa spesifik pada tipe tertentu, misalnya …int
pada fungsi Min
yang memilih bilang terendah dari sejumlah bilangan integer:
func Min(a ...int) int { min := int(^uint(0) >> 1) // largest int for _, i := range a { if i < min { min = i } } return min }
Append
Sekarang kita memiliki pengetahuan yang diperlukan untuk menjelaskan rancangan
dari fungsi bawaan append
.
Fungsi append
berbeda dari fungsi bentukan Append
di atas.
Secara skematis, bentuknya seperti berikut:
func append(slice []T, elements ...T) []T
yang mana T
adalah penampung dari tipe apapun.
Anda tidak bisa menulis sebuah fungsi dalam Go yang mana tipe T
ditentukan
oleh yang memanggil.
Itulah kenapa append
adalah bawaan: ia butuh dukungan dari compiler.
Yang append
lakukan adalah menambahkan elemen-elemen pada akhir dari sebuah
slice dan mengembalikan hasilnya.
Hasilnya perlu dikembalikan karena, seperti pada fungsi bentukan Append
,
array yang dikandung oleh slice kemungkinan berubah.
Contoh sederhana berikut
x := []int{1,2,3} x = append(x, 4, 5, 6) fmt.Println(x)
mencetak [1 2 3 4 5 6]
.
Jadi append
bekerja mirip seperti Printf
, mengoleksi beragam jumlah
argumen.
Namun bagaimana bila kita ingin menambahkan slice ke sebuah slice?
Mudah: gunakan …
pada saat pemanggilan, seperti yang kita lakukan pada
Output
sebelumnya.
Potongan kode berikut menghasilkan keluaran yang sama dengan yang di atas.
x := []int{1,2,3} y := []int{4,5,6} x = append(x, y...) fmt.Println(x)
Tanpa …
, kode tersebut tidak akan bisa di- compile karena tipenya salah;
y
bukan bertipe int
.
Inisialisasi
Walaupun tampak tidak berbeda dengan inisialisasi dalam C atau C++, inisialisasi pada Go lebih kuat. Struktur yang kompleks dapat dibangun lewat inisialisasi dan permasalahan urutan dari objek yang diinisialisasi, bahkan untuk paket-paket yang berbeda, ditangani dengan benar.
Konstan
Konstan pada Go dibuat pada waktu di- compile (bahkan saat didefinisikan
sebagai lokal konstan dalam fungsi), dan hanya berlaku untuk bilangan,
karakter (rune
), string, atau boolean.
Karena batasan dari compile-time tersebut, ekspresi yang mendefinisikan
mereka haruslah ekspresi konstan, yang dapat di evaluasi oleh compiler.
Misalnya, 1<<3
adalah ekspresi konstan, sementara math.Sin(math.Pi/4)
bukan konstan karena pemanggilan fungsi math.Sin
harus terjadi pada saat
program berjalan.
Pada Go, konstan enumerasi dibuat menggunakan operator iota
(disebut juga
dengan enumerator).
Secara iota
merupakan bagian dari ekspresi dan ekspresi secara implisit bisa
diulang, maka sangat mudah untuk membuat sekumpulan nilai intrinsik.
type ByteSize float64 const ( _ = iota // indahkan nilai pertama (0) dengan memberikannya // pada pengidentifikasi kosong KB ByteSize = 1 << (10 * iota) MB GB TB PB EB ZB YB )
Adanya method seperti String
yang bisa ditambahkan ke tipe kostum apapun
membuatnya memungkinan bagi nilai yang beragam untuk memformat dirinya
sendiri untuk pencetakan.
Meskipun akan sering melihatnya hanya digunakan 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 1.00YB
, sementara ByteSize(1e13)
mencetak 9.09TB
.
Penggunaan Sprintf
untuk mengimplementasikan method String
pada
ByteSize
adalah aman (menghindari rekursif tanpa henti) bukan karena
konversi itu sendiri namun karena Sprintf
dengan %f
, yang mana bukan
format dari sebuah string: Sprintf
hanya akan memanggil method String
saat ia menginginkan sebuah string, dan %f
menginginkan nilai
floating-point.
Variabel
Variabel bisa diinisialisasi seperti konstan namun penginisialisasinya bisa sebuah ekspresi generik yang dieksekusi saat program berjalan.
var ( home = os.Getenv("HOME") user = os.Getenv("USER") gopath = os.Getenv("GOPATH") )
Fungsi init
Terakhir, setiap berkas sumber kode bisa mendefinisikan fungsi init
-nya
sendiri untuk mempersiapkan apapun yang dibutuhkan.
(Sebenarnya, setiap berkas bisa memiliki lebih dari satu fungsi init
.)
Fungsi init
dipanggil setelah semua deklarasi variabel dalam paket tersebut
telah dievaluasi, dan setelah semua paket yang diimpor telah diinisialisasi.
Selain membuat inisialisasi yang tidak dapat diekspresikan sebagai deklarasi,
penggunaan umum dari fungsi init
adalah untuk memverifikasi atau memperbaiki
keadaan dari 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 may be overridden by --gopath flag on command line. flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH") }
Method
Pointer dan Nilai
Seperti yang kita lihat pada ByteSize
, method dapat didefinisikan untuk
tipe apapun (kecuali pointer atau interface);
receiver -nya tidak harus sebuah struct
.
Pada diskusi mengenai slice di atas, kita menulis sebuah fungsi Append
.
Kita bisa mendefinisikannya sebagai sebuah method dari slice.
Untuk itu, pertama kita mendeklarasikan sebuah tipe bernama yang akan
ditambahkan dengan method, dan menjadikan receiver -nya sebagai nilai dari
tipe tersebut.
type ByteSlice []byte func (slice ByteSlice) Append(data []byte) []byte { // Badan fungsi sama dengan fungsi Append yang didefinisikan di atas. }
Yang dimaksud dengan receiver dari contoh di atas yaitu slice ByteSlice
.
Pada contoh tersebut receiver -nya berbentuk receiver dengan nilai, atau
singkatnya, receiver nilai.
Cara ini masih membutuhkan method untuk mengembalikan slice yang diperbarui.
Kita bisa menghilangkan nilai kembalian dengan mendefinisikan ulang method
tersebut dengan menggunakan pointer pada ByteSlice
sebagai receiver
-nya, sehingga method dapat menimpa slice.
func (p *ByteSlice) Append(data []byte) { slice := *p // Badan fungsi seperti sebelunya, tanpa kembalian. ... *p = slice }
Jika kita mengubah fungsi Append
tersebut seperti standar method Write
,
seperti berikut,
func (p *ByteSlice) Write(data []byte) (n int, err error) { slice := *p // . *p = slice ... return len(data), nil }
maka tipe *ByteSlice
memenuhi interface
standar io.Writer
.
Misalnya, kita dapat mencetak ke dalam ByteSlice
.
var b ByteSlice fmt.Fprintf(&b, "This hour has %d days\n", 7)
Kita mengirim alamat dari sebuah ByteSlice
karena hanya *ByteSlice
yang
memenuhi io.Writer
.
Aturan tentang pointer atau nilai untuk receiver yaitu receiver
nilai dapat memanggil method yang dideklarasikan dengan pointer dan nilai,
tapi receiver pointer hanya dapat memanggil method yang dideklarasikan
sebagai pointer.
Aturan ini ada karena method dengan pointer dapat mengubahi receiver;
memanggil method dengan nilai akan menyebabkani method tersebut menerima
salinan dari nilai, sehingga perubahan apapun akan diindahkan.
Oleh karena itu, bahasa Go tidak membolehkan kesalahan ini.
Ada sebuah pengecualian.
Saat nilai dapat diambil alamatnya, bahasa Go akan mengurus pemanggilan
method dengan pointer dengan menyisipkan operator alamat secara otomatis.
Dalam contoh di atas, variabel b
dapat diambil alamatnya, sehingga kita
dapat memanggil method Write
-nya dengan b.Write
.
Compiler akan mengubahnya menjadi (&b).Write
.
Selain itu, gagasan menggunakan Write
terhadap sebuah byte slice adalah
pusat dari implementasi bytes.Buffer
.
Interface dan tipe lainnya
Interface
Dalam Go, interface menyediakan sebuah cara untuk menentukan perilaku dari
sebuah objek: "jika sesuatu dapat melakukan ini, maka ia dapat digunakan di
sini."
Kita telah melihat beberapa contohnya; pencetakan kostum yang
diimplementasikan dengan method String
, Fprintf
yang dapat membangkitkan
keluaran apapun dengan method Write
.
Interface dengan satu atau dua method sangat umum dalam kode Go, dan
biasanya diberikan nama yang diturunkan dari nama method -nya, seperti
io.Writer
untuk sesuatu yang mengimplementasikan Write
.
Sebuah tipe dapat mengimplementasikan banyak interface.
Misalnya, sebuah koleksi dapat diurutkan oleh fungsi-fungsi dalam paket
sort
jika ia mengimplementasikan sort.Interface
, yang terdiri dari
Len()
, Less(i, j int) bool
, dan Swap(i, j int)
, dan ia juga bisa
memiliki sebuah fungsi pencetakan tersendiri.
Pada contoh berikut Sequence
memenuhi kedua interface tersebut.
type Sequence []int // Method-method 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...) } // String mengembalikan elemen yang diurutkan sebelum dicetak. func (s Sequence) String() string { s = s.Copy() // Buat salinan; jangan menimpa receiver. sort.Sort(s) str := "[" for i, elem := range s { // Pengulangan ini O(N²); akan kita perbaiki nanti. if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" }
Konversi
Method String
pada tipe kostum Sequence
di atas membuat ulang fungsi
Sprint
yang dapat bekerja pada slice.
(Ia juga memiliki kompleksitas O(N²), yang mana sangat lambat.)
Kita dapat mengurangi kode tersebut (dan mempercepatnya) jika kita
mengonversi Sequence
menjadi []int
sebelum memanggil Sprint
.
func (s Sequence) String() string { s = s.Copy() sort.Sort(s) return fmt.Sprint([]int(s)) }
Method di atas adalah contoh lain dari teknik konversi untuk dapat memanggil
Sprintf
dengan aman dari sebuah method String
.
Karena kedua tipe (Sequence
dan []int
) adalah sama (jika kita indahkan
nama tipe), maka boleh mengonversi antara mereka.
Konversi tidak membuat nilai baru, ia berperilaku seperti nilai yang ada
memiliki tipe baru.
(Ada beberapa konversi legal lainnya, seperti dari integer ke bilangan
float, namun membuat nilai baru).
Adalah sebuah idiom dalam program Go untuk mengonversi tipe dari sebuah
ekspresi untuk mengakses sekumpulan method yang berbeda.
Sebagai contoh, kita dapat menggunakan tipe sort.IntSlice
untuk mengurangi
contoh kode menjadi:
type Sequence []int // String untuk pencetakan - urutkan elemen-elemen sebelum dicetak. func (s Sequence) String() string { s = s.Copy() sort.IntSlice(s).Sort() return fmt.Sprint([]int(s)) }
Sekarang, daripada membuat Sequence
mengimplementasikan beberapa interface
(pengurutan dan pencetakan), kita menggunakan kemampuan sebuah item data untuk
dikonversi ke beragam tipe (Sequence
, sort.IntSlice
, dan []int
),
tiap-tiapnya melakukan bagian kerjanya sendiri.
Hal seperti ini jarang biasanya dalam dunia nyata namun efektif.
Konversi interface dan pernyataan tipe
Switch dengan tipe adalah sebuah bentuk dari konversi: ia
mengambil sebuah interface dan, untuk setiap case
pada switch
, mencoba
mengonversinya ke tipe di dalam case
tersebut.
Jika sebuah interface adalah string, kita ingin nilai string sebenarnya,
namun bila ia memiliki method String
kita ingin nilai dari pemanggilan
method String()
.
type Stringer interface { String() string } var value interface{} // Value provided by caller. switch str := value.(type) { case string: return str case Stringer: return str.String() }
case
yang pertama mencari nilai string sebenarnya;
case
yang kedua mengonversi interface ke dalam interface yang lain.
Adalah hal yang biasa menggabungkan tipe seperti di atas.
Bagaimana jika hanya satu tipe yang kita inginkan?
Jika kita tahu bahwa nilai dari interface benar-benar string dan kita hanya
ingin mengekstraksinya?
Hal ini dapat dilakukan dengan switch
bertipe dengan satu case
, cara
lainnya yaitu dengan pernyataan tipe langsung.
Pernyataan tipe langsung menerima sebuah nilai interface dan mengekstrak
sebuah nilai dari tipe eksplisitnya.
Sintaksnya diturunkan dari klausa pembukaan dari switch
bertipe, tapi dengan
tipe eksplisit bukan dengan kata kunci type
:
value.(typeName)
dan hasilnya adalah nilai baru bertipe statis typeName
.
Tipe tersebut haruslah tipe nyata yang dipegang oleh interface, atau sebuah
tipe interface yang nilainya dapat dikonversi.
str := value.(string)
Namun bila nilainya bukanlah sebuah string
, program akan eror.
Untuk mengatasi ini, gunakan idiom "koma, ok" untuk menguji, secara aman,
apakah nilainya adalah sebuah string:
str, ok := value.(string) if ok { fmt.Printf("nilai string dari value: %q\n", str) } else { fmt.Printf("value bukanlah sebuah string\n") }
Jika pernyataan tipe gagal, str
akan tetap ada dan bertipe string, namun
nilainya akan kosong (sebuah string
kosong).
Sebagai ilustrasi dari kemampuan dari pernyataan tipe langsung, berikut sebuah
perintah if-else
yang sama dengan switch
bertipe seperti contoh pada
bagian awal.
if str, ok := value.(string); ok { return str } else if str, ok := value.(Stringer); ok { return str.String() }
Generalisasi
Jika sebuah tipe ada hanya untuk mengimplementasikan sebuah interface dan tidak akan memiliki method yang diekspor (selain dari method pada interface) maka tidak perlu mengekspor tipe tersebut. Mengekspor interface saja memperjelas bahwa nilainya tidak memiliki makna selain yang dideskripsikan dalam interface. Ia juga mengurangi perlunya mengulang dokumentasi untuk setiap instansi dari method.
Dalam kasus seperti ini, konstruktor sebaiknya mengembalikan sebuah nilai
interface bukan mengimplementasikan tipe.
Sebagai contohnya, dalam pustaka crc32.NewIEEE
dan adler32.New
mengembalikan interface dengan tipe hash.Hash32
.
Mengganti sebuah algoritma dari CRC-32 ke Adler-32 dalam sebuah program Go
membutuhkan hanya perubahan dari pemanggilan konstruksi;
sisa kode selebihnya tidak terpengaruh dengan perubahan algoritma.
Pendekatan yang sama membuat algoritma cipher dalam berbagai paket crypto
menjadi terpisah dari blok cipher yang mengikatnya.
Interface Block
dalam paket crypto/cipher
menentukan perilaku dari
sebuah blok cipher, yang menyediakan enkripsi dari sebuah blok data.
Kemudian, secara analogi dengan paket bufio
, paket cipher
yang
mengimplementasikan interface ini dapat digunakan untuk membangun aliran
cipher, direpresentasikan oleh interface Stream
, tanpa perlu tahu
rincian dari blok enkripsinya.
Interface dari crypto/cipher
berbentuk seperti berikut:
type Block interface { BlockSize() int Encrypt(src, dst []byte) Decrypt(src, dst []byte) } type Stream interface { XORKeyStream(dst, src []byte) }
Berikut definisi dari aliran counter mode (CTR), yang mengubah blok cipher menjadi aliran cipher; perhatikan bahwa detil dari blok cipher diabstraksikan:
// NewCTR returns a Stream that encrypts/decrypts using the given Block in // counter mode. The length of iv must be the same as the Block's block size. func NewCTR(block Block, iv []byte) Stream
NewCTR
tidak hanya berlaku untuk algoritma enkripsi dan sumber data tertentu
namun untuk semua implementasi dari interface Block
dan Stream
.
Karena ia mengembalikan nilai interface, mengganti enkripsi CTR
dengan
mode enkripsi lainnya menjadi perubahan dilokal saja.
Pemanggilan dari konstruksi haruslah diubah, namun karena kode yang
disekelilingnya harus memperlakukan kembalian sebagai sebuah Stream
, maka
perubahannya tidak terlalu banyak terlihat.
Interface dan method
Secara semua tipe kostum dapat memiliki method, maka hampir semuanya
memenuhi sebuah interface.
Salah satu contohnya adalah dalam paket http
, yang mendefinisikan
interface Handler
.
Objek apapun yang mengimplementasikan Handler
dapat melayani permintaan
HTTP.
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
ResponseWriter
itu sendiri adalah sebuah interface yang menyediakan akses
terhadap method yang diperlukan untuk mengembalikan respon kepada klien
(HTTP).
Method-method tersebut termasuk standar Write
, jadi http.ResponseWriter
bisa digunakan dimanapun io.Writer
dapat digunakan.
Request
adalah sebuah struct
yang berisi representasi hasil penguraian
permintaan HTTP dari klien.
Untuk lebih jelas, mari kita indahkan POST dan asumsikan permintaan HTTP selalu GET; penyederhanaan ini tidak mempengaruhi bagaimana handler disiapkan. Berikut implementasi lengkap dari sebuah handler untuk menghitung jumlah sebuah halaman dikunjungi.
// Simple counter server. 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) }
(Perhatikan bagaimana Fprintf
dapat mencetak ke http.ResponseWriter
.)
Sebagai referensi, berikut cara menyambungkan sebuah server terhadap sebuah
URL.
import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr)
Tapi kenapa Counter
itu adalah sebuah struct
?
Sebuah integer saja sebenarnya sudah cukup.
(receiver haruslah pointer supaya penambahan dapat dilihat oleh
pemanggilnya.)
// Simpler counter server. type Counter int func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { *ctr++ fmt.Fprintf(w, "counter = %d\n", *ctr) }
Bagaimana bila program memiliki sebuah keadaan internal yang perlu
diberitahu bila sebuah halaman telah dikunjungi?
Sambungkan sebuah channel
ke halaman web tersebut.
// Chan adalah sebuah channel yang mengirim notifikasi setiap kali halaman // dikunjungi. (channel bisa diberi _buffer_.) type Chan chan *http.Request func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { ch <- req fmt.Fprint(w, "notifikasi terkirim") }
Terakhir, katakanlah kita ingin menampilkan semua argumen yang digunakan saat
menjalankan program server dalam halaman yang diakses lewat URL /args
.
Cukup mudah untuk menulis sebuah fungsi yang mencetak argumen tersebut.
func ArgServer() { fmt.Println(os.Args) }
Terus bagaimana cara mengubahnya menjadi sebuah server HTTP?
Kita bisa buat ArgServer
sebuah method dari tipe yang nilainya kita
indahkan, namun ada cara yang lebih bersih.
Karena kita dapat mendefinisikan sebuah method untuk semua tipe kecuali
pointer dan interface, maka kita dapat menulis sebuah method untuk
sebuah fungsi.
Paket http
memiliki contoh kode seperti ini:
// The HandlerFunc type is an adapter to allow the use of // ordinary functions as HTTP handlers. If f is a function // with the appropriate signature, HandlerFunc(f) is a // Handler object that calls f. type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, req). func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) { f(w, req) }
HandlerFunc
adalah sebuah tipe dengan sebuah method, ServerHTTP
,
sehingga nilai dari tipe tersebut dapat melayani permintaan HTTP.
Lihat implementasi dari method tersebut: receiver -nya adalah sebuah
fungsi, f
, dan method -nya memanggil f
kembali.
Hal ini tampak ganjil tapi tidak ada bedanya dengan, katakanlah, receiver
-nya adalah sebuah channel
dan method mengirim ke channel
tersebut.
Untuk membuat ArgServer
menjadi sebuah server HTTP, pertama kita ubah
supaya memiliki penanda yang sesuai.
// Argument server. func ArgServer(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, os.Args) }
ArgServer
sekarang memiliki penanda yang sama dengan HandlerFunc
, sehingga
ia bisa dikonversi ke tipe tersebut untuk mengakses method -nya, seperti
saat kita mengonversi Sequence
ke IntSlice
untuk mengakses
IntSlice.Sort
.
Kode untuk mengatur semua ini cukup ringkas:
http.Handle("/args", http.HandlerFunc(ArgServer))
Saat seseorang mengunjungi halaman /args
, handler yang terpasang pada
halaman tersebut memiliki nilai ArgServer
dan bertipe HandlerFunc
.
Server HTTP akan memanggil method ServeHTTP
dalam tipe tersebut, dengan
ArgServer
sebagai receiver -nya, yang mana akan memanggil ArgServer
(lewat f(w, req)
di dalam HandlerFunc.ServeHTTP
).
Argumen dari program nanti akan ditampilkan.
Dalam bagian ini kita telah membuat sebuah server HTTP dari sebuah struct
,
sebuah integer, sebuah channel
, dan sebuah fungsi, semua itu karena
interface hanyalah sekumpulan method, yang dapat didefinisikan untuk
(hampir) semua tipe.
Pengidentifikasi kosong
Kita telah membaca tentang pengidentifikasi kosong beberapa kali, dalam
konteks
pengulangan `for range`
dan
map.
Pengidentifikasi kosong dapat diset atau dideklarasikan dengan nilai dan tipe
apapun, dengan nilai yang diabaikan tanpa mempengaruhi kode.
Ia seperti menulis ke berkas /dev/null
pada sistem operasi turunan Unix:
ia merepresentasikan nilai yang hanya dapat dibaca saja yang digunakan sebagai
penampung untuk variabel yang dibutuhkan tapi nilai sebenarnya tidak relevan.
Ada lebih banyak penggunaan pengidentifikasi kosong dari yang telah kita lihat
sebelumnya.
Pengidentifikasi kosong pada penempatan multi
Penggunaan pengidentifikasi kosong dalam sebuah pengulangan for range
adalah
kasus khusus dari situasi umum: penempatan multipel.
Jika sebuah penempatan membutuhkan nilai yang banyak pada bagian kiri, namun satu dari nilai tersebut tidak akan digunakan pada program, pengidentifikasi kosong pada bagian kiri penempatan mengabaikan perlunya membuat sebuah variabel dan memperjelas bahwa nilai tersebut diabaikan. Misalnya, saat memanggil sebuah fungsi yang mengembalikan nilai dan eror, namun hanya eror yang diperlukan, gunakan pengidentifikasi kosong untuk mengindahkan nilai yang tidak penting.
if _, err := os.Stat(path); os.IsNotExist(err) { fmt.Printf("%s does not exist\n", path) }
Terkadang kita akan melihat kode yang mengabaikan nilai eror; hal ini adalah cara yang tidak bagus. Selalu periksa kembalian eror; mereka ada karena alasan tertentu.
// Buruk! Program akan eror jika path tidak ada. fi, _ := os.Stat(path) if fi.IsDir() { fmt.Printf("%s is a directory\n", path) }
Variabel dan impor yang tidak dipakai
Go akan eror saat mengimpor paket atau mendeklarasikan variabel yang tidak dipakai. Impor yang tidak dipakai akan mengembungkan program dan memperlambat kompilasi, sementara variabel yang diinisialisasi tapi tidak dipakai setidaknya memboroskan komputasi dan bisa saja mengindikasikan sebuah bug yang besar. Saat sebuah program dalam keadaan aktif dikembangkan, variabel dan impor yang tidak dipakai terkadang muncul dan ia terkadang bisa menjengkelkan untuk menghapusnya hanya supaya kompilasi dapat berjalan, namun dibutuhkan lagi nanti. Pengidentifikasi kosong menyediakan solusi untuk masalah ini.
Program setengah-jadi berikut memiliki dua impor tidak dipakai (fmt
dan
io
) dan sebuah variabel tak dipakai (fd
), sehingga ia tidak bisa
dikompilasi, namun cukup bagus untuk melihat jika kode sejauh ini 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 melenyapkan komplain tentang impor tak terpakai, gunakan
pengidentifikasi kosong untuk mengacu ke simbol dari paket yang diimpor.
Cara yang sama, penempatan pada variabel tak terpakai fd
ke pengidentifikasi
kosong akan melenyapkan eror dari variabel tak terpakai.
Versi program berikut dapat di- compile.
package main import ( "fmt" "io" "log" "os" ) var _ = fmt.Printf // For debugging; delete when done. var _ io.Reader // For debugging; delete when done. func main() { fd, err := os.Open("test.go") if err != nil { log.Fatal(err) } // TODO: use fd. _ = fd }
Secara konvensinya, deklarasi global untuk melenyapkan eror pada impor
sebaiknya setelah deklarasi import
dan diberi komentar, supaya mudah dicari
dan sebagai pengingat untuk dibersihkan nantinya.
Impor sebagai efek samping
Impor yang tak terpakai seperti fmt
atau io
pada contoh sebelumnya
seharusnya digunakan lagi kembali atau dihapus: penempatan kosong
mengindikasikan kode sedang dikerjakan.
Namun terkadang berguna mengimpor paket hanya untuk efek sampingnya, tanpa ada
penggunaan eksplisit.
Sebagai contohnya, dalam fungsi init
, paket net/http/pprof
meregistrasi
beberapa handler untuk HTTP yang menyediakan informasi untuk melakukan
debug.
Ia memiliki API yang diekspor, namun kebanyakan klien hanya memerlukan
registrasi handler dan akses ke data lewat halaman web.
Untuk mengimpor paket hanya untuk efek sampingnya, ubah nama paket menjadi
pengidentifikasi kosong:
import _ "net/http/pprof"
Bentuk import
seperti ini memperjelas bahwa paket diimpor untuk efek
sampingnya, karena tidak ada penggunaan langsung dari paket tersebut: dalam
berkas ini, ia tidak memiliki sebuah nama.
(Jika memiliki nama, dan kita tidak menggunakannya, maka compiler akan
menolak program saat di- compile.)
Pemeriksaan interface
Seperti yang telah kita lihat dalam diskusi tentang
interface di atas,
sebuah tipe tidak perlu secara eksplisit mendeklarasikan bahwa ia
mengimplementasikan sebuah interface.
Sebuah tipe dikatakan implementasi dari interface bila ia
mengimplementasikan semua method dari interface.
Pada praktiknya, konversi dari interface adalah statis dan oleh karena itu
diperiksa saat program di- compile.
Sebagai contohnya, mengirim *os.File
ke sebuah fungsi yang mengharapkan
io.Reader
tidak akan bisa di- compile kecuali bila *os.File
mengimplementasikan interface dari io.Reader
.
Beberapa pemeriksaan interface terjadi pada saat program berjalan.
Salah satu contohnya yaitu dalam paket encoding/json
, yang mendefinisikan
sebuah interface Marshaler
.
Saat encoder dari JSON menerima sebuah nilai yang mengimplementasikan
interface tersebut, encoder memanggil method untuk melakukan konversi
untuk mengubahnya ke JSON bukan dengan melakukan konversi biasa.
encoder memeriksa properti tersebut saat program berjalan dengan
pengecekan tipe seperti:
m, ok := val.(json.Marshaler)
Jika perlu untuk memeriksa apakah sebuah tipe mengimplementasikan sebuah interface, tanpa perlu menggunakan interface itu sendiri, mungkin sebagai bagian dari pemeriksaan eror, gunakan pengidentifikasi kosong untuk mengabaikan nilai yang ditempatkan dari tipe:
if _, ok := val.(json.Marshaler); ok { fmt.Printf("nilai %v dari tipe %T mengimplementasikan json.Marshaler\n", val, val) }
Salah satu situasi seperti ini bisa muncul adalah saat diperlukannya jaminan
pada paket yang mengimplementasikan tipe bahwa ia benar-benar memenuhi
interface.
Jika sebuah tipe—sebagai contoh, json.RawMessage
—membutuhkan sebuah
representasi kostum dari JSON, ia seharusnya mengimplementasikan
json.Marshaler
, tapi tidak ada konversi statis yang dapat menyebabkan
compiler untuk dapat memverifikasi hal ini secara otomatis.
Jika tipe secara tidak sengaja gagal memenuhi interface, encoder JSON
tetap bekerja, namun tidak akan menggunakan implementasi kostum.
Untuk menjamin supaya implementasinya benar, sebuah deklarasi global
menggunakan pengidentifikasi kosong dapat digunakan dalam paket:
var _ json.Marshaler = (*RawMessage)(nil)
Dalam deklarasi ini, penempatan mengikutkan sebuah konversi dari *RawMessage
ke Marshaler
mengharuskan *RawMessage
mengimplementasikan Marshaler
, dan
properti tersebut akan diperiksa saat program dikompilasi.
Jika interface dari json.Marshaler
berubah, paket ini tidak akan bisa
di- compile lagi dan sebagai pemberitahuan bahwa ia perlu diperbarui.
Adanya pengidentifikasi kosong dalam konstruksi di atas mengindikasikan bahwa deklarasi hanya ada untuk pemeriksaan tipe, bukan untuk membuat sebuah variabel. Namun, jangan lakukan hal ini untuk setiap tipe yang memenuhi sebuah interface. Secara konvensi, deklarasi seperti itu hanya digunakan bila tidak ada konversi statis yang ada dalam kode, yang mana biasanya jarang terjadi.
Penanaman (embedding)
Go tidak menyediakan notasi sub-class, tapi memiliki kemampuan untuk
"meminjam" sebuah implementasi dengan menanamkan tipe dalam sebuah struct
atau interface
.
Penanaman dalam interface cukup mudah.
Kita telah melihat interface dari io.Reader
dan io.Writer
sebelumnya;
berikut definisi mereka.
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 interface lain yang menspesifikasikan
objek yang dapat mengimplementasikan beberapa dari method tersebut.
Misalnya, ada io.ReadWriter
, sebuah interface yang berisi Read
dan
Write
.
Kita dapat menspesifikasikan io.ReadWriter
dengan mendaftarkan kedua
method secara eksplisit, tapi lebih mudah bila menanamkan kedua interface
untuk membentuk sebuah interface baru, seperti:
// ReadWriter adalah interface yang menggabungkan interface Reader dan Writer. type ReadWriter interface { Reader Writer }
Interface tersebut mengatakan: Sebuah ReadWriter
dapat melakukan apa yang
Reader
lakukan dan apa yang Writer
lakukan;
ia adalah gabungan dari interface yang ditanam (yang mana harus berupa
kumpulan disjoint dari method).
Hanya interface yang dapat ditanam ke dalam interface.
Hal yang sama berlaku juga kepada struct
, namun dengan implikasi lebih
lanjut.
Paket bufio
memiliki dua tipe struct
, bufio.Reader
dan bufio.Writer
,
setiapnya mengimplementasikan interface dari paket io
.
Dan bufio
juga mengimplementasikan reader / writer dengan buffer,
dengan menggabungkan sebuah reader dan sebuah writer ke dalam sebuah
struct
menggunakan penanaman: ia mendaftarkan tipe-tipe tersebut ke dalam
struct
tanpa memberi nama pada bagian field -nya.
// ReadWriter menyimpan pointer ke sebuah Reader dan sebuah Writer. // Ia mengimplementasikan io.ReadWriter. type ReadWriter struct { *Reader // *bufio.Reader *Writer // *bufio.Writer }
Elemen yang tertanam adalah pointer ke struct
dan tentu saja harus
diinisialisasi supaya menunjuk ke struct
yang valid sebelum dapat digunakan.
struct
ReadWriter
dapat ditulis dengan cara
type ReadWriter struct { reader *Reader writer *Writer }
namun untuk mempromosikan method dari field dan supaya memenuhi
interface io
, kita juga perlu menyediakan method penerus, seperti
berikut:
func (rw *ReadWriter) Read(p []byte) (n int, err error) { return rw.reader.Read(p) }
Dengan menanam struct
tersebut secara langsung, kita menghindari kode di
atas.
Method dari tipe yang ditanam juga ikut dibawa, artinya bufio.ReadWriter
tidak hanya memiliki method dari bufio.Reader
dan bufio.Writer
, ia juga
memenuhi ketiga interface: io.Reader
, io.Writer
, dan io.ReadWriter
.
Ada sebuah hal penting yang mana penanaman berbeda dari sub-class.
Saat kita menanam sebuah tipe, method dari tipe tersebut menjadi method
dari tipe di luarnya, namun saat dipanggil, receiver dari method adalah
tipe di dalamnya, bukan tipe yang di luarnya.
Dalam contoh berikut, saat method Read
dari bufio.ReadWriter
dipanggil,
ia memiliki efek yang sama seperti meneruskan method seperti yang ditulis di
atas;
bagian receiver adalah field reader
dari ReadWriter
, bukan
ReadWriter
itu sendiri.
Penanaman bisa menyederhanakan kode. Contoh berikut memperlihatkan field yang ditanam bersama dengan field yang biasa, yang memiliki nama.
type Job struct { Command string *log.Logger }
Tipe Job
sekarang memiliki Print
, Printf
, dan Println
dan method
lainnya yang diturunkan dari *log.Logger
.
Kita bisa saja memberikan nama pada field Logger
, namun untuk hal ini
tidak diperlukan.
Dan setelah diinisialisasi, kita dapat melakukan log dengan Job
:
job.Println("starting now...")
Logger
adalah field biasa dari struct Job
, jadi kita bisa inisialisasi
dengan cara biasa di dalam pembangun dari Job
, seperti berikut,
func NewJob(command string, logger *log.Logger) *Job { return &Job{command, logger} }
atau dengan komposit,
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
Jika kita butuh untuk mengacu ke field yang ditanam secara langsung, nama
tipe dari field, tanpa nama paket, dapat digunakan sebagai nama
field, seperti pada method Read
dari struct ReadWriter
kita.
Dalam contoh di atas, untuk mengakses *log.Logger
dari sebuah variabel Job
bernama job
, kita bisa tulis dengan job.Logger
, yang mana sangat berguna
jika kita ingin mengubah method dari Logger
.
func (job *Job) Printf(format string, args ...interface{}) { job.Logger.Printf("%q: %s", job.Command, fmt.Sprintf(format, args...)) }
Menanam tipe menimbulkan sebuah permasalahan konflik pada nama, namun beberapa
aturan untuk mengatasi masalah ini cukup sederhana.
Pertama, sebuah field atau method X
menyembunyikan item X
lainnya di
dalam sub-tipe.
Jika log.Logger
memiliki sebuah field atau method bernama Command
,
field Command
dari Job
akan mendominasinya.
Kedua, jika nama yang sama ada pada tingkat yang sama, ia biasanya akan eror;
adalah sebuah kesalahan untuk menanam log.Logger
jika struct Job
itu
sendiri berisi field atau method lainnya bernama Logger.
Jika nama yang duplikat tidak pernah disebut dalam program selain dari
definisi dari tipe, ia tidak akan bermasalah.
Kualifikasi seperti ini menyediakan sebuah proteksi terhadap perubahan dari
tipe yang ditanam dari luar;
tidak ada masalah jika sebuah field yang ditambahkan konflik dengan field
lain di dalam sub-tipe lainnya jika field tersebut tidak pernah digunakan.
Konkurensi
Berbagi dengan berkomunikasi
Pemrograman konkuren adalah topik yang luas dan bagian ini khusus untuk menjelaskan cara melakukan pemrograman konkuren hanya untuk Go.
Pemrograman konkuren pada bahasa pemrograman lain menjadi susah dengan
peliknya kebutuhan untuk mengimplementasikan akses yang benar untuk variabel
yang dibagi.
Go menyarankan pendekatan yang berbeda yang mana nilai yang dibagi dikirim
lewat channel
dan, tidak pernah dibagi dengan eksekusi thread yang lain.
Hanya ada satu goroutine yang memiliki akses terhadap nilai dalam satu
waktu.
Konflik pada data tidak bakal terjadi, secara sengaja.
Untuk mendorong cara berpikir seperti ini kita menyederhanakannya menjadi
sebuah slogan:
Jangan berkomunikasi dengan berbagi memory; namun, bagilah memory dengan berkomunikasi.
Pendekatan ini bisa berlebihan.
Penghitungan referensi lebih baik dilakukan dengan menggunakan mutex dalam
sebuah variabel integer, misalnya.
Namun dalam pendekatan yang lebih tinggi, menggunakan channel
untuk
mengontrol akses membuatnya lebih mudah untuk menulis program yang benar dan
jelas.
Salah satu cara untuk berpikir dengan model seperti ini yaitu dengan membayangkan program dalam satu thread yang berjalan dalam satu CPU. Ia tidak memerlukan sinkronisasi primitif. Sekarang coba jalankan instansi yang sama; ia juga tidak membutuhkan sinkronisasi. Sekarang buat mereka berkomunikasi; jika komunikasi adalah sebuah sinkronisasi, maka tidak perlu ada lagi sinkronisasi yang lain. Pipeline pada Unix, sebagai contohnya, cocok dengan model ini. Pendekatan konkurensi pada Go berasal dari Communicating Sequential Processes (CSP) pada Hoare, ia juga bisa dilihat sebagai generalisasi tipe pipe pada Unix.
Goroutine
Dinamakan goroutine karena istilah yang ada seperti thread, coroutine, proses, dan lainnya, membawa konotasi yang tidak akurat. Sebuah goroutine memiliki model sederhana: ia adalah sebuah fungsi yang dieksekusi secara bersamaan dengan goroutine lainnya dalam ruang alamat yang sama. Goroutine ringan, membutuhkan tidak lebih dari alokasi dari ruang stack. Dan stack -nya dimulai dari ukuran yang kecil, sehingga ia cukup murah, dan berkembang dengan mengalokasikan (dan melepaskan) penyimpanan pada heap seperlunya.
Goroutine disebar ke berbagai thread pada sistem operasi, sehingga bila salah satu dari mereka diblok, seperti menunggu untuk masukan/keluaran, yang lainnya akan tetap berjalan. Rancangan dari goroutine menyembunyikan kompleksitas dari pembuatan dan manajemen thread.
Untuk menggunakan goroutine, awali pemanggilan fungsi atau method dengan
kata kunci go
.
Saat fungsi atau method selesai, goroutine akan berhenti dengan sendirinya.
(Efeknya mirip dengan notasi &
pada Unix shell untuk menjalankan sebuah
perintah secara terpisah.)
go list.Sort() // jalankan list.Sort secara bersamaan; tidak perlu ditunggu.
Sebuah fungsi anonim dapat juga dipanggil dengan goroutine.
func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(message) }() // Perhatikan tanda kurung - fungsi harus dipanggil. }
Dalam Go, fungsi anonim adalah sebuah closure: implementasi harus memastikan variabel yang diacu oleh fungsi tersebut bertahan selama mereka aktif.
Contoh di atas tidak terlalu praktis karena fungsi tidak memiliki cara untuk
memberi sinyal pada saat berhenti.
Untuk itu, kita membutuhkan channel
.
Channel
Seperti map
, channel
dialokasikan dengan make
, dan nilai kembaliannya
adalah referensi ke struktur data internalnya.
Jika sebuah parameter integer diberikan, ia akan menset ukuran buffer dari
channel
.
Nilai bakunya adalah nol, untuk channel
tanpa buffer dan bersifat
sinkron.
ci := make(chan int) // integer channel tanpa buffer cj := make(chan int, 0) // integer channel tanpa buffer cs := make(chan *os.File, 100) // File pointer channel dengan buffer
channel
tanpa buffer menggabungkan komunikasi (pertukaran sebuah nilai)
dengan sinkronisasi—menjamin dua buah kalkulasi (goroutine) berada dalam
status yang tetap.
Ada banyak idiom menarik dari penggunaan channel
.
Berikut salah satunya.
Dalam bagian sebelumnya kita melakukan pengurutan dengan proses yang
dijalankan terpisah, di belakang.
Sebuah channel
membolehkan peluncuran goroutine untuk menunggu pengurutan
selesai.
c := make(chan int) // Allocate a channel. // Mulai pengurutan dalam goroutine; saat selesai, beri sinyal ke channel. go func() { list.Sort() c <- 1 // Kirimkan sebuah sinyal; nilainya bisa apapun. }() doSomethingForAWhile() <-c // Tunggu list.Sort selesai; indahkan nilai yang diterima.
Penerima selalu diblok sampai ada data yang diterima.
Jika channel
tanpa buffer, maka pengirim diblok sampai penerima
menerima nilainya.
Jika channel
memiliki buffer, pengirim diblok hanya sampai nilai telah
disalin ke buffer;
jika buffer -nya penuh, berarti menunggu sampai penerima setidaknya telah
menerima sebuah nilai.
channel
dengan buffer dapat digunakan untuk
semafor,
misalnya untuk membatasi hasil keluaran.
Pada contoh berikut, permintaan yang masuk dikirim ke sebuah handle, yang
mengirim sebuah nilai ke channel
, memproses permintaan, dan kemudian
menerima sebuah nilai dari channel
untuk menyiapkan semafor bagi konsumer
selanjutnya.
Kapasitas dari buffer pada channel
membatasi jumlah pemanggilan proses
secara bersamaan.
var sem = make(chan int, MaxOutstanding) func handle(r *Request) { sem <- 1 // Tunggu antrian yang aktif kosong. process(r) // proses bisa jadi butuh waktu lama. <-sem // Selesai; siapkan permintaan selanjutnya untuk diproses. } func Serve(queue chan *Request) { for { req := <-queue go handle(req) // Jangan tunggu handle selesai. } }
Saat sejumlah handler diproses sebanyak MaxOutstanding
, handler
selanjutnya akan diblok, sampai salah satu handler selesai.
Kode diatas memiliki sebuah masalah: fungsi Serve
membuat sebuah goroutine
untuk setiap permintaan yang masuk, walaupun dibatasi oleh jumlah
MaxOutstanding
yang dapat berjalan bersamaan.
Akibatnya, program dapat mengkonsumsi sumber daya tanpa batas jika permintaan
datang terlalu cepat.
Kita dapat mengatasi ini dengan mengubah Serve
membatasi pembuatan dari
goroutine.
Berikut solusinya, namun ia memiliki bug yang akan kita perbaiki lagi nanti:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) // Ada bug; lihat penjelasan di bawah. <-sem }() } }
Bug -nya ada dalam pengulangan for
, variabel lokal req
pada pengulangan
digunakan pada setiap iterasi, sehingga variabel req
dibagi dengan setiap
goroutine.
Hal seperti ini bukanlah yang kita inginkan.
Kita harus memastikan bahwa req
adalah unik untuk setiap goroutine.
Berikut salah satu cara untuk melakukannya, dengan mengirim nilai dari req
sebagai argumen dari closure dalam goroutine:
func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func(req *Request) { process(req) <-sem }(req) } }
Bandingkan versi ini dengan sebelumnya untuk melihat perbedaan bagaimana closure dideklarasikan dan dijalankan. Solusi lainnya adalah dengan membuat variabel baru dengan nama yang sama, seperti pada contoh berikut:
func Serve(queue chan *Request) { for req := range queue { req := req // Buat instansi baru dari req untuk goroutine. sem <- 1 go func() { process(req) <-sem }() } }
Tampak ganjil saat menulis
req := req
namun hal seperti ini legal dan idiomatis dalam Go. Kita mendapatkan versi variabel yang baru dengan nama yang sama, yang dengan sengaja menutup variabel pada pengulangan secara lokal namun unik untuk setiap goroutine.
Kembali lagi ke permasalahan tentang membuat server tadi, pendekatan lain
untuk mengatur sumber daya yaitu dengan menjalankan sejumlah goroutine
terhadap handler yang semuanya membaca dari channel
permintaan.
Jumlah goroutine membatasi jumlah dari pemanggilan proses secara bersamaan.
Fungsi Serve
berikut menerima sebuah channel
yang mana memberitahunya
untuk berhenti;
setelah meluncurkan goroutine, ia akan berhenti menerima dari channel
tersebut.
func handle(queue chan *Request) { for r := range queue { process(r) } } func Serve(clientRequests chan *Request, quit chan bool) { // Jalankan sejumlah goroutine untuk handlers for i := 0; i < MaxOutstanding; i++ { go handle(clientRequests) } <-quit // Tunggu sampai diberitahu untuk keluar. }
Channel dari channel
Salah satu properti penting dari Go yaitu sebuah channel
adalah nilai yang
bisa dialokasikan dan dikirim seperti nilai lainnya.
Penggunaan umum dari properti ini yaitu untuk mengimplementasikan
demultiplexing yang paralel dan aman.
Pada contoh sebelumnya, handle menangani permintaan tapi kita tidak
mendefinisikan tipe yang ditanganinya.
Jika tipe tersebut mengikutkan sebuah channel
untuk mengirim balasan,
setiap klien memiliki jalurnya sendiri untuk mengirim balasan.
Berikut definisi skematis dari tipe Request
.
type Request struct { args []int f func([]int) int resultChan chan int }
Klien menyediakan sebuah fungsi dan argumennya, berikut dengan sebuah
channel
di dalam objek permintaan yang akan menerima balasan.
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 balasannya. fmt.Printf("jawaban: %d\n", <-request.resultChan)
Di sisi server, hanya fungsi penanganannya yang berubah.
func handle(queue chan *Request) { for req := range queue { req.resultChan <- req.f(req.args) } }
Tentu saja banyak yang harus dilakukan untuk membuatnya lebih realistis, namun kode di atas adalah sebuah kerangka untuk sistem RPC dengan batasan, paralel, tanpa diblok; tanpa adanya penggunaan sebuah mutex.
Paralelisasi
Penerapan lainnya dari gagasan di atas yaitu untuk memparalelkan kalkulasi
diantara core pada multipel CPU.
Jika kalkulasi dapat dipecah menjadi bagian-bagian yang dapat dieksekusi
secara independen, maka ia dapat diparalelkan, dengan sebuah channel
untuk
memberi sinyal saat bagian tersebut selesai.
Katakanlah kita memiliki sebuah operasi yang mahal untuk dilakukan pada sejumlah besar item, dan nilai dari operasi dari setiap item adalah independen, seperti pada contoh berikut.
type Vector []float64 // Lakukan operasi pada v[i], v[i+1] ... sampai 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 // beri sinyal bahwa bagian ini selesai. }
Kita luncurkan setiap bagian secara independen di dalam sebuah pengulangan,
satu per CPU.
Mereka dapat selesai tanpa berurutan;
kita cukup hitung sinyal yang selesai dengan menguras channel
setelah
meluncurkan semua goroutine.
const numCPU = 4 // jumlah core pada CPU func (v Vector) DoAll(u Vector) { c := make(chan int, numCPU) // Opsi pembufferan for i := 0; i < numCPU; i++ { go v.DoSome(i*len(v)/numCPU, (i+1)*len(v)/numCPU, u, c) } // Drain the channel. for i := 0; i < numCPU; i++ { <-c // tunggu untuk satu pekerjaan selesai. } // Semua pekerjaan telah selesai. }
Daripada menggunakan sebuah nilai konstan untuk numCPU
, kita dapat
menanyakan pada runtime nilai yang sesuai dengan sistem kita.
Fungsi
runtime.NumCPU
mengembalikan jumlah core CPU pada mesin, jadi kita bisa tulis
var numCPU = runtime.NumCPU()
Ada juga fungsi
runtime.GOMAXPROCS,
yang melaporkan (atau menset) jumlah core yang dispesifikasikan oleh
pengguna untuk membatasi jumlah core yang dapat berjalan secara bersamaan
dalam sebuah program Go.
Nilai bawaannya yaitu nilai dari runtime.NumCPU
namun dapat ditimpa dengan
menset variabel lingkungan dengan nama yang sama GOMAXPROCS
atau dengan
memanggil fungsi tersebut dengan nilai positif.
Memanggilnya dengan nilai nol akan mengembalikan nilai yang dikandungnya.
Oleh karena itu, untuk menghargai permintaan sumber dari pengguna, kita
sebaiknya menulis
var numCPU = runtime.GOMAXPROCS(0)
Pastikan tidak bingung dengan gagasan dari konkurensi (membentuk sebuah program untuk mengeksekusi komponen-komponen secara independen) dan paralelisme (mengeksekusi kalkulasi dengan paralel untuk efisiensi multipel CPU.) Walaupun fitur konkurensi dari Go dapat mempermudah membentuk komputasi secara paralel, Go adalah bahasa yang konkuren, bukan paralel, dan tidak semua permasalahan paralelisasi cocok dengan model Go. Untuk diskusi mengenai perbedaan dari keduanya, lihat pembicaraan dalam blog berikut.
Buffer yang lepas
Perkakas dari pemrograman konkuren dapat membuat pemrograman non-konkuren
mudah diekspresikan.
Berikut sebuah contoh yang diabstraksikan dari paket RPC.
Pengulangan pada klien goroutine menerima data dari beberapa sumber, bisa dari
jaringan.
Untuk menghindari alokasi dan pelepasan buffer, ia menyimpannya dalam sebuah
daftar buffer, dan menggunakan channel
dengan buffer untuk
merepresentasikannya. Jika channel
kosong, sebuah buffer baru
dialokasikan.
Saat pesan buffer siap digunakan, ia dikirim ke server lewat serverChan
.
var freeList = make(chan *Buffer, 100) var serverChan = make(chan *Buffer) func client() { for { var b *Buffer // Ambil sebuah buffer jika ada; alokasikan bila tidak ada. select { case b = <-freeList: // Dapata satu buffer; default: // Tidak ada buffer yang lepas, alokasikan yang baru. b = new(Buffer) } load(b) // Baca pesan selanjutnya dari jaringan. serverChan <- b // Kirim ke server. } }
Pengulangan pada server menerima setiap pesan dari klien, memprosesnya, dan mengembalikan buffer lewat daftar buffer.
func server() { for { b := <-serverChan // Wait for work. process(b) // Gunakan buffer jika ada ruang yang tersisa. select { case freeList <- b: // Buffer ada dalam daftar. default: // Daftar buffer penuh, lanjutkan. } } }
Klien mencoba mengambil sebuah buffer dari freeList
;
jika tidak ada, ia akan mengalokasikan yang baru.
Server menaruh b
kembali ke freeList
kecuali bila daftar buffer telah
penuh, maka buffer b
akan dibebaskan untuk diklaim oleh
garbage collector .
(Klausa default
pada perintah switch
dieksekusi bila tidak ada case
yang
siap atau bernilai true
, artinya perintah select
tidak akan pernah
diblok.)
Implementasi seperti ini membangun sebuah daftar penampung yang lepas hanya
dengan beberapa baris kode, yang bergantung pada channel
dengan buffer dan
garbage collector.
Error
Fungsi atau method dari sebuah pustaka terkadang harus mengembalikan sebuah
indikasi eror ke pemanggilnya.
Seperti yang telah disebut sebelumnya, kembalian multi nilai pada Go
mempermudah mengembalikan sebuah nilai kembalian biasa bersama dengan
nilai eror.
Penggunaan fitur ini sangat baik untuk menyediakan informasi lebih rinci dari
eror.
Misalnya, os.Open
tidak saja mengembalikan pointer nil
bila gagal, ia
juga mengembalikan sebuah nilai eror yang menjelaskan apa yang terjadi.
Secara konvensi, sebuah eror bertipe error
, sebuah interface bawaan.
type error interface { Error() string }
Penulis pustaka bebas mengimplementasikan interface ini dengan model yang
lebih kaya dibelakangnya, membuatnya memungkinkan untuk melihat pesan eror dan
menyediakan semacam konteks.
Seperti yang disebutkan sebelumnya, bersamaan dengan nilai kembalian normal
*os.File
, os.Open
juga mengembalikan nilai error
.
Jika sebuah berkas dibuka dengan sukses, error
akan bernilai nil
, namun
bila ada masalah, ia akan berisi os.PathError
:
// PathError mencatat sebuah eror dan operasi beserta berkas path yang // menyebabkannya. type PathError struct { Op string // "open", "unlink", dll. Path string // Berkas yang menyebabkan eror. Err error // Nilai eror yang dikembalikan oleh pemanggilan ke sistem. } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
Pesan eror dari PathError
bentuknya seperti berikut:
open /etc/passwx: no such file or directory
Eror seperti di atas, yang mengikutkan nama berkas yang bermasalah, operasi, dan sistem eror yang mentrigger, berguna bila dicetak dari pemanggilnya; ia lebih informatif daripada "no such file or directory".
Bila memungkinan, pesan eror sebaiknya mengidentifikasi asal muasalnya,
seperti dengan memberikan awalan nama operasi atau paket yang membangkitkan
eror.
Misalnya, dalam paket image
, representasi pesan dari eror saat decoding
yang disebabkan oleh format yang tidak dikenal adalah "image: unknown format".
Pemanggil fungsi yang peduli dengan rincian eror yang sebenarnya dapat
menggunakan sebuah switch
bertipe atau konversi tipe untuk mencari nilai
spesifik dari eror dan mengekstrak rinciannya.
Untuk PathErrors
hal ini bisa dengan memeriksa field internal Err
untuk
dapat memulihkan dari kesalahan.
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() // Recover some space. continue } return }
Perintah if
kedua adalah
konversi tipe.
Jika gagal, ok
akan bernilai false
, dan e
akan nil
.
Jika sukses, ok
bernilai true
, artinya eror err
bertipe *os.PathError
,
dan begitu juga dengan e
, yang dapat kita periksa informasi erornya lebih
lanjut.
Panic
Cara yang umum untuk melaporkan sebuah eror ke pemanggilnya adalah dengan
mengembalikan sebuah error
sebagai nilai kembalian tambahan.
Method Read
adalah salah satu contohnya;
ia mengembalikan jumlah byte dan error
.
Namun bagaimana jika eror tidak bisa ditangani?
Terkadang program sebaiknya tidak dilanjutkan (dipaksa berhenti saat itu
juga).
Untuk tujuan ini, ada fungsi bawaan panic
yang efeknya membuat sebuah eror
run-time yang akan menghentikan program (namun lihat juga seksi berikutnya).
Fungsi panic
menerima sebuah argumen bertipe apapun—biasanya
string
—untuk dicetak saat program mati.
Cara ini untuk mengindikasikan bahwa sesuatu yang tidak diharapkan terjadi,
seperti keluar dari pengulangan tanpa batas.
// Implementasi akar pangkat tiga menggunakan metoda Newton. func CubeRoot(x float64) float64 { z := x/3 // Arbitrary initial value for i := 0; i < 1e6; i++ { prevz := z z -= (z*z*z-x) / (3*z*z) if veryClose(z, prevz) { return z } } // Sejuta iterasi belum selesai; kemungkinan ada yang salah. panic(fmt.Sprintf("CubeRoot(%g) did not converge", x)) }
Kode di atas hanyalah contoh namun fungsi-fungsi dalam sebuah pustaka
seharusnya tidak menggunakan panic
.
Jika kesalahan dapat ditutupi atau diperbaiki, akan lebih baik untuk
dilanjutkan daripada menghentikan seluruh program.
Salah satu contohnya yaitu saat inisialisasi: jika sebuah pustaka tidak
dapat menset dirinya sendiri, maka masuk akal untuk panic
.
var user = os.Getenv("USER") func init() { if user == "" { panic("no value for $USER") } }
Recover
Saat panic
dipanggil, misalnya karena adanya kesalahan akses indeks pada
slice atau kegagalan saat melakukan konversi tipe, Go runtime langsung
menyetop eksekusi dari fungsi dan mulai memutar ulang stack dari goroutine,
dan menjalankan fungsi-fungsi yang ditunda dengan defer
disaat bersamaan.
Jika proses pemutaran ulang mencapai tingkat paling atas dari stack
goroutine, program akan mati.
Namun, memungkinan menggunakan fungsi bawaan recover
untuk mengambil kontrol
dari goroutine dan mengulangi eksekusi secara normal.
Pemanggilan recover
menyetop pemutaran ulang dan mengembalikan argumen yang
dikirim saat pemanggilan panic
.
Secara kode yang berjalan selama proses pemutaran ulang adalah fungsi-fungsi
yang di- defer
, recover
hanya berguna dalam fungsi yang di- defer
.
Salah satu penerapan dari recover
yaitu menutup goroutine yang gagal di
dalam sebuah server tanpa menghentikan 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) }
Pada contoh ini, jika do(work)
ternyata panic
, kembaliannya akan dicatat
dan goroutine akan keluar dengan bersih tanpa mengganggu yang lainnya.
Tidak perlu melakukan hal lain dalam closure yang di- defer
;
memanggil recover
akan menangani kondisi tersebut sepenuhnya.
Karena recover
selalu mengembalikan nil
kecuali dipanggil langsung dari
fungsi yang di- defer
, kode yang di- defer
dapat memanggil fungsi pustaka
yang di dalamnya juga menggunakan panic
dan recover
tanpa gagal.
Sebagai contoh, fungsi defer
pada safelyDo
bisa saja memanggil fungsi
pencatatan log.Println
sebelum memanggil recover
, dan kode pencatatan
tersebut akan berjalan tanpa dipengaruhi oleh keadaan yang panic
.
Dengan pola pemulihan seperti ini, fungsi do
(dan apapun yang dipanggilnya)
dapat keluar dari situasi yang buruk dengan bersih dengan memanggil panic
.
Kita dapat menggunakan pola ini untuk menyederhanakan penanganan eror dalam
sebuah perangkat lunak yang kompleks.
Mari kita lihat versi ideal dari sebuah paket regexp
, yang melaporkan
eror penguraian dengan memanggil panic
dengan tipe eror lokal.
Berikut definisi dari Error
, sebuah method error
, dan fungsi Compile
.
// Error adalah tipe dari sebuah eror penguraian; ia memenuhi interface error. type Error string func (e Error) Error() string { return string(e) } // error adalah sebuah method dari *Regexp yang melaporkan eror penguraian // lewat panic. func (regexp *Regexp) error(err string) { panic(Error(err)) } // Compile mengembalikan sebuah representasi dari regular expression yang // telah diurai. func Compile(str string) (regexp *Regexp, err error) { regexp = new(Regexp) // doParse akan panic bila ada eror saat penguaraian. defer func() { if e := recover(); e != nil { regexp = nil // Bersihkan nilai kembalian. err = e.(Error) // Akan panic walaupun bukan eror penguraian. } }() return regexp.doParse(str), nil }
jika doParse
panik, blok pemulihan akan menset nilai kembalian menjadi
nil
—fungsi yang di- defer
dapat mengubah nilai kembalian bernama.
Ia kemudian akan memeriksa, pada saat penempatan ke err
, bahwa
permasalahannya adalah eror penguraian dengan mengkonversi ke tipe Error
lokal.
Jika konversi tipe gagal err = e.(Error)
, menyebabkan eror run-time yang
melanjutkan pemutaran ulang stack.
Pemeriksaan ini artinya jika sesuatu yang tidak diharapkan terjadi, seperti
pengaksesan indeks di luar batas, kode tersebut akan tetap panik walaupun kita
menggunakan panic
dan recover
untuk menangani eror pada saat penguraian.
Dengan adanya penanganan eror, method error
(karena ia adalah sebuah
method yang terikat ke sebuah tipe, maka ia dibolehkan memiliki nama yang
sama dengan tipe error
bawaan) mempermudah melaporkan eror penguraian tanpa
khawatir pada proses pemutaran ulang pada stack:
if pos == 0 { re.error("'*' illegal at start of expression") }
Pola seperti ini sebaiknya digunakan hanya dalam sebuah paket.
Parse
mengubah pemanggilan internal panic
-nya menjadi nilai error
;
ia tidak mengekspos panik ke klien.
Hal tersebut adalah sebuah aturan yang bagus untuk diikuti.
Idiom dari re- panic
ini mengubah nilai panic
jika eror sebenarnya
terjadi.
Namun, kedua kesalahan yang asli dan baru akan tetap tercatat dalam laporan
crash, sehingga akar penyebab dari permasalahan akan tetap dapat dilihat.
Maka pendekatan sederhana dari re- panic
biasanya cukup efisien—ia
sebenarnya sebuah crash—tapi jika kita hanya ingin menampilkan nilai
aslinya, kita dapat menulis sedikit kode tambahan untuk memfilter permasalahan
yang tidak terduga dan mengulang panic
dengan nilai error
aslinya.
Hal ini merupakan latihan bagi pembaca.
Sebuah server web
Mari kita tutup dokumentasi ini dengan membuat sebuah program web server.
Web server kita ini sebenarnya adalah sejenis server web yang memanggil
layanan di web server lainnya, yaitu layanan grafik dari Google.
Google menyediakan sebuah layanan di chart.apis.google.com
yang melakukan
pemformatan secara otomatis dari data yang dikirim menjadi sebuah grafik atau
bagan.
Layanan tersebut tidak mudah digunakan, karena kita harus menaruh data ke
dalam sebuah URL dalam bentuk HTTP query.
Program kita ini menyediakan sebuah antar muka dari layanan tersebut dengan
menyediakan sebuah form data: berikan sebuah teks, web server kita akan akan
memanggil layanan grafik Google untuk menghasilkan sebuah
kode QR,
hasil dari enkode teks tersebut.
Kode QR tersebut dapat dipindai dengan kamera ponsel pintar kita dan
diinterpretasikan sebagai sebuah URL, mengurangi pengetikan ulang URL ke dalam
keyboard telepon yang kecil.
Berikut kode dari program web server kita. Diikuti oleh penjelasan setelahnya.
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 kode pada fungsi main
seharusnya cukup mudah diikuti.
Sebuah flag mengatur port standar dari HTTP bagi server kita, yaitu
1718
.
Variabel template templ
adalah bagian yang menarik dari kode ini.
Ia membangun sebuah template HTML yang akan dieksekusi oleh server kita
untuk ditampilkan dalam halaman.
Fungsi main
mengurai flag dan, menggunakan mekanisme seperti yang telah
kita diskusikan sebelumnya, mengikat fungsi QR
ke path /
dari server
kita.
Kemudian http.ListenAndServe
dipanggil untuk memulai server;
yang akan memblok fungsi sampai server selesai berjalan.
Fungsi QR
menerima permintaan, yang terdiri dari sebuah form data, dan
mengeksekusi template dengan input berupa nilai form bernama s
.
Paket html/template
sangat berguna;
program ini hanya menyentuh bagian luar dari kemampuannya.
Intinya, template menulis ulang sebuah teks HTML dengan mengganti elemen
yang diturunkan dari item data yang dikirim ke templ.Execute
, dalam kasus
ini nilai dari form
.
Di dalam teks template (templateStr
), bagian dengan tanda kurung kurawal
ganda menandakan sebuah aksi.
Bagian dari {{if .}}
sampai dengan {{end}}
dieksekusi hanya jika nilai
dari item data yang sekarang, disebut .
(titik), tidak kosong.
Jika stringnya kosong, bagian kode template ini disembunyikan.
Bagian kedua dari {{.}}
menyatakan untuk menampilkan data yang
direpresentasikan oleh nilai .
ke dalam template—string dari query—pada
halaman web.
Paket HTML template secara otomatis menyediakan pembersihan data inputnya
sendiri sehingga teks aman untuk ditampilkan.
Sisa dari teks template yaitu elemen HTML untuk ditampilkan saat halaman
dimuat.
Jika penjelasan ini terlalu cepat, lihat
dokumentasi
untuk paket template
untuk penjelasan yang lebih lengkap.
Untuk mencoba kode di atas, simpanlah ke dalam sebuah berkas berekstensi .go
dalam sebuah
ruang kerja dan jalankan,
$ go run .
kemudian buka peramban web pada halaman http://127.0.0.1:1718`.
Dan dengan ini kita memiliki sebuah web server yang berguna, dengan sejumlah baris kode ditambah teks HTML. Go sangat ampuh untuk membuat banyak hal terjadi dalam beberapa baris kode.