Efektif Go

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, sehingga

	x<<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 <code>doc</code> dari perkakas <code>go</code> 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.

MixedCaps

Terakhir, konvensi dalam Go yaitu menggunakan MixedCaps atau mixedCaps untuk penamaan dengan beberapa kata bukan dengan garis bawah.

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 (jika v 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]<&lt;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 https://id.wikipedia.org/wiki/Semafor_(pemrograman)[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 = `
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;QR Link Generator&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
{{if .}}
&lt;img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" /&gt;
&lt;br&gt;
{{.}}
&lt;br&gt;
&lt;br&gt;
{{end}}
&lt;form action="/" name=f method="GET"&gt;&lt;input maxLength=1024 size=70
name=s value="" title="Text to QR Encode"&gt;&lt;input type=submit
value="Show QR" name=qr&gt;
&lt;/form&gt;
&lt;/body&gt;
&lt;/html&gt;
`

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.