Pendahuluan
Pada bulan September 2010 kami memperkenalkan Go Playground, sebuah layanan web yang mengompilasi dan mengeksekusi kode Go dan menampilkan keluarannya.
Jika Anda adalah pemrogram Go maka kemungkinan Anda sudah pernah menggunakan playground lewat Go Playground secara langsung, melakukan Tur Go, atau menjalankan contoh dari dokumentasi Go.
Anda mungkin juga pernah pakai lewat tombol "Run" dalam salah satu presentasi di talks.golang.org atau artikel dalam situs ini (seperti artikel tentang String).
Dalam artikel ini kita akan melihat bagaimana playground diimplementasikan dan diintegrasikan dengan layanan-layanan tersebut. Implementasi mengikutkan beragam lingkungan sistem operasi dan runtime dan penjelasan kita sekarang mengasumsikan Anda akrab dengan pemrograman sistem menggunakan Go.
Ikhtisar
Layanan playground memiliki tiga bagian:
-
Sebuah back-end yang berjalan di server Google. Ia menerima permintaan RPC, mengompilasi program menggunakan perkakas go, mengeksekusi program, dan mengembalikan keluaran program (atau kesalahan kompilasi) sebagai respons RPC.
-
Sebuah front-end yang berjalan di Google App Engine. Ia menerima permintaan HTTP dari klien dan membuat permintaan RPC ke back-end. Ia juga melakukan beberapa caching.
-
Sebuah klien JavaScript yang mengimplementasikan antar muka dan membuat permintaan HTTP ke front-end.
Back-end
Program back-end itu sendiri cukup biasa, jadi kita tidak akan mendiskusikan implementasinya di sini. Bagian yang menarik yaitu bagaimana kita mengeksekusi kode secara aman dalam sebuah lingkungan terjaga dengan tetap menyediakan fungsionalitas inti seperti waktu, jaringan, dan sistem berkas.
Untuk mengisolasi program pengguna dari infrastruktur Google, back-end
menjalankan program dengan
Native Client
(atau "NaCl"),
sebuah teknologi yang dikembangkan oleh Google untuk membolehkan eksekusi
program x86 dengan aman di dalam peramban.
Back-end menggunakan versi khusus dari perkakas go
yang menghasilkan program
NaCl yang dapat dieksekusi.
(Perkakas khusus ini telah digabungkan ke Go 1.3. Untuk belajar lebih lanjut, bacalah dokumentasi rancangan .)
NaCl membatasi jumlah CPU dan RAM yang dikonsumsi program, dan mencegah program dari mengakses jaringan atau sistem berkas. Hal ini menimbulkan sebuah masalah. Dukungan konkurensi dan jaringan dalam Go adalah beberapa dari kunci kekuatannya, dan akses ke sistem berkas adalah hal yang vital bagi banyak program. Untuk dapat mendemonstrasikan konkurensi secara efektif kita membutuhkan akses ke waktu pada sistem, dan untuk mendemonstrasikan jaringan dan sistem berkas kita membutuhkan akses ke sebuah jaringan dan sebuah sistem berkas.
Walaupun semuanya telah didukung sekarang ini, versi pertama dari playground,
yang diluncurkan tahun 2010, tidak memiliki semua hal tersebut.
Waktu sekarang di-set baku ke 10 November 2009, time.Sleep
tidak memiliki
efek, dan kebanyakan fungsi dalam paket os
dan net
dimatikan dengan
mengembalikan eror EINVALID
.
Setahun yang lalu kami mengimplementasikan
waktu palsu
dalam playground, supaya program yang memanggil Sleep
dapat
bekerja dengan benar.
Pembaruan terakhir dari playground memperkenalkan jaringan palsu dan sistem
berkas palsu, membuat perkakas playground hampir sama dengan perkakas Go yang
normal.
Fasilitas-fasilitas tersebut dijelaskan dalam bagian-bagian di bawah ini.
Pemalsuan waktu
Program-program yang berjalan di playground dibatasi jumlah waktu CPU dan memori yang dapat mereka gunakan, tetapi mereka juga dibatasi dalam berapa banyak waktu yang dapat mereka gunakan. Hal ini karena setiap program yang berjalan mengonsumsi sumber pada back-end dan pada infrastruktur antara klien dan back-end. Membatasi run-time dari setiap program di playground membuat layanan kita lebih dapat diprediksi dan menjaga kita dari serangan denial of service.
Namun batasan ini menjadi bermasalah saat kode yang dieksekusi menggunakan
fungsi-fungsi waktu.
Wicara
Pola Konkurensi Go
mendemonstrasikan konkurensi dengan contoh-contoh yang menggunakan fungsi
waktu seperti
time.Sleep
dan
time.After
.
Bila dijalankan pada versi playground yang lama, maka Sleep
pada program
tidak berpengaruh dan perilakunya akan aneh (dan terkadang salah).
Dengan menggunakan sebuah trik kita dapat membuat sebuah program Go berpikir bahwa ia tertidur, padahal sebenarnya peniduran ini tidak memakan waktu sama sekali. Untuk dapat menjelaskan trik ini kita harus memahami bagaimana penjadwal (scheduler) mengatur goroutine yang tertidur.
Saat sebuah goroutine memanggil time.Sleep
(atau fungsi yang mirip) si
scheduler menambahkan sebuah timer ke sebuah heap dari timer yang
ditunda dan menempatkan goroutine ke mode tidur.
Sementara itu, sebuah goroutine timer khusus mengatur heap tersebut.
Saat goroutine timer berjalan ia memberitahu si scheduler untuk
membangunkannya saat timer yang tertunda selanjutnya telah siap berjalan dan
kemudian mulai tidur.
Saat ia bangun, ia akan memeriksa timer mana saja yang telah kedaluwarsa,
dan membangunkan goroutine tersebut, dan lanjut tidur kembali.
Triknya adalah dengan mengubah kondisi yang membangunkan goroutine timer. Bukan dengan bangun setelah periode waktu tertentu, kita memodifikasi si scheduler untuk menunggu sampai sebuah deadlock, keadaan yang mana semua goroutine diblok.
Versi playground dari runtime mengatur waktu internalnya sendiri. Saat si scheduler mendeteksi sebuah deadlock ia akan memeriksa apakah ada timer yang tertunda. Jika ada, ia memajukan waktu internal ke waktu trigger dari timer paling awal dan kemudian membangunkan goroutine timer. Eksekusi terus berjalan dan program percaya bahwa waktu telah lewat, pada kenyataannya waktu tidur berjalan secara instan.
Pemalsuan waktu memperbaiki beberapa isu dari penggunaan sumber daya pada back-end, tetapi bagaimana dengan keluaran program? Akan aneh melihat sebuah program yang tertidur berjalan sampai selesai dengan benar tanpa menghabiskan waktu sama sekali.
Program berikut mencetak waktu saat ini setiap detik dan kemudian keluar setelah tiga detik. Coba jalankan.
func main() { stop := time.After(3 * time.Second) tick := time.NewTicker(1 * time.Second) defer tick.Stop() for { select { case <-tick.C: fmt.Println(time.Now()) case <-stop: return } } }
Bagaimana ia bekerja? Ia bekerja dengan kolaborasi antara back-end, front-end, dan klien.
Kita menangkap tempo pada setiap penulisan ke standar keluaran dan eror dan mengirimnya ke klien. Kemudian klien dapat "menjalankan ulang" penulisan tersebut dengan tempo yang tepat, sehingga keluaran muncul seperti program berjalan secara benar di lokal komputer Anda.
Paket runtime pada playground menyediakan sebuah fungsi
write
khusus
yang mengikutkan sebuah "playback header" sebelum setiap penulisan.
Playback header tersebut berisi sebuah string, waktu sekarang, dan panjang
data yang ditulis.
Sebuah penulisan dengan playback header memiliki struktur berikut:
0 0 P B <8-byte time> <4-byte data length> <data>
Keluaran mentah dari program seperti di atas bentuknya seperti berikut:
\x00\x00PB\x11\x74\xef\xed\xe6\xb3\x2a\x00\x00\x00\x00\x1e2009-11-10 23:00:01 +0000 UTC \x00\x00PB\x11\x74\xef\xee\x22\x4d\xf4\x00\x00\x00\x00\x1e2009-11-10 23:00:02 +0000 UTC \x00\x00PB\x11\x74\xef\xee\x5d\xe8\xbe\x00\x00\x00\x00\x1e2009-11-10 23:00:03 +0000 UTC
Front-end membaca keluaran tersebut sebagai sekumpulan even dan mengembalikan daftar even tersebut ke klien sebagai sebuah objek JSON:
{ "Errors": "", "Events": [ { "Delay": 1000000000, "Message": "2009-11-10 23:00:01 +0000 UTC\n" }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:02 +0000 UTC\n" }, { "Delay": 1000000000, "Message": "2009-11-10 23:00:03 +0000 UTC\n" } ] }
Klien JavaScript (yang berjalan dalam peramban pengguna) kemudian menjalankan setiap even menggunakan interval delay yang diberikan. Dari sisi pengguna ia tampak seperti program yang berjalan seperti biasa.
Pemalsuan sistem berkas
Program yang dibangun dengan perkakas Go NaCl tidak dapat mengakses sistem
berkas dari mesin lokal.
Namun fungsi-fungsi pada paket syscall
yang berkaitan dengan berkas (seperti
Open
, Read
, Write
, dan seterusnya) beroperasi dalam sistem berkas di
dalam memori yang diimplementasikan oleh paket syscall
itu sendiri.
Secara paket syscall
adalah antar muka antara kode Go dan kernel pada sistem
operasi, program melihat sistem berkas persis seperti yang mereka lihat di
dunia nyata.
Contoh program berikut menulis data ke sebuah berkas, dan kemudian menyalin isi berkas tersebut ke standar keluaran. Cobalah jalankan. (Anda bisa mengubahnya juga!)
func main() { const filename = "/tmp/file.txt" err := ioutil.WriteFile(filename, []byte("Hello, file system\n"), 0644) if err != nil { log.Fatal(err) } b, err := ioutil.ReadFile(filename) if err != nil { log.Fatal(err) } fmt.Printf("%s", b) }
Saat proses mulai berjalan, sistem berkas diisi dengan beberapa perangkat di
bawah /dev
dan sebuah direktori /tmp
yang kosong.
Program dapat memanipulasi sistem berkas seperti biasa, tetapi saat program
selesai setiap perubahan pada sistem berkas akan hilang.
Ada juga sebuah perlengkapan yang memuat berkas zip ke dalam sistem berkas pada saat init (lihat unzip_nacl.go). Sejauh ini kita hanya menggunakan fasilitas unzip untuk menyediakan berkas-berkas data yang dibutuhkan untuk menjalankan pustaka standar pengujian, tetapi kami ingin menyediakan program playground dengan sekumpulan berkas yang dapat digunakan dalam contoh-contoh dokumentasi, artikel blog, dan tur Go.
Implementasinya dapat ditemukan dalam berkas
fs_nacl.go
dan
fd_nacl.go
(yang dibangun ke paket syscall
hanya bila GOOS
di set ke nacl
).
Sistem berkas itu sendiri direpresentasikan oleh
struct fsys,
yang mana sebuah instansi global (bernama fs
) dibuat saat inisiasi.
Beragam fungsi-fungsi berkas beroperasi terhadap fs
bukan melakukan
pemanggilan sistem secara langsung.
Misalnya, berikut fungsi
syscall.Open:
func Open(path string, openmode int, perm uint32) (fd int, err error) { fs.mu.Lock() defer fs.mu.Unlock() f, err := fs.open(path, openmode, perm&0777|S_IFREG) if err != nil { return -1, err } return newFD(f), nil }
Penanda berkas (file descriptor) dilacak oleh sebuah slice global bernama
files
.
Setiap penanda berkas berkorespondensi dengan sebuah
file
dan setiap file
menyediakan nilai yang mengimplementasikan interface
fileImpl
.
Ada beberapa implementasi dari interface tersebut:
-
Berkas dan perangkat biasa (seperti
/dev/random
) direpresentasikan olehfsysFile
, -
Standar masukan, keluaran, dan eror adalah instansi dari
naclFile
, yang menggunakan pemanggilan sistem untuk berinteraksi dengan berkas yang sebenarnya (hal ini adalah satu-satunya cara program berinteraksi dengan dunia luar), -
socket jaringan memiliki implementasinya sendiri, yang didiskusikan pada bagian bawah.
Pemalsuan jaringan
Seperti pada sistem berkas, jaringan komputer pada playground adalah sebuah
pemalsuan proses yang diimplementasikan oleh paket syscall
.
Ia membolehkan playground menggunakan interface loopback (127.0.0.1
).
Permintaan ke host yang lain akan gagal.
Sebagai contoh, jalankan program berikut. Ia akan membuka port TCP, menunggu koneksi yang masuk, menyalin data dari koneksi tersebut ke standar keluaran, dan keluar. Di dalam goroutine yang lain, ia membuat koneksi ke port yang terbuka tersebut, menulis sebuah string ke koneksi, dan menutupnya.
func main() { l, err := net.Listen("tcp", "127.0.0.1:4000") if err != nil { log.Fatal(err) } defer l.Close() go dial() c, err := l.Accept() if err != nil { log.Fatal(err) } defer c.Close() io.Copy(os.Stdout, c) } func dial() { c, err := net.Dial("tcp", "127.0.0.1:4000") if err != nil { log.Fatal(err) } defer c.Close() c.Write([]byte("Hello, network\n")) }
Antarmuka dari jaringan lebih kompleks dari sistem berkas, sehingga implementasi dari pemalsuan jaringan lebih besar dan kompleks daripada pemalsuan sistem berkas. Pemalsuan jaringan harus dapat menyimulasikan timeout baca dan tulis, tipe-tipe alamat dan protokol yang berbeda, dan seterusnya.
Implementasi tersebut dapat ditemukan dalam
net_nacl.go.
Bagian awal yang bagus untuk dibaca yaitu
netFile,
implementasi dari jaringan socket dari interface fileImpl
.
Front-end
Front-end dari playground adalah program sederhana (kurang dari 100 baris). Ia menerima permintaan HTTP dari klien, membuat permintaan RPC ke back-end, dan melakukan semacam penyimpanan sementara (caching).
Front-end melayani HTTP dengan sebuah handler di
golang.org/compile.
Handler tersebut menerima permintaan POST dengan bagian body
(program Go
yang akan dijalankan) dan kolom version
opsional (untuk klien pada umumnya
nilainya "2").
Saat front-end menerima permintaan kompilasi, pertama ia akan memeriksa memcache untuk melihat apakah ia pernah menyimpan hasil kompilasi dari kode yang sama sebelumnya. Jika ditemukan, ia akan mengembalikan respons yang tersimpan tersebut. Cache tersebut mencegah program yang umum seperti yang dapat kita temukan pada halaman depan Go dari menghabiskan sumber daya pada back-end. Jika tidak ada respons yang tersimpan, front-end melakukan pemanggilan RPC ke back-end, menyimpan respons ke dalam memcache, membaca even-event, dan mengembalikan sebuah objek JSON ke klien sebagai respons HTTP (seperti yang dijelaskan di atas).
Klien
Beragam situs yang menggunakan playground memiliki kode JavaScript yang sama untuk menyiapkan antarmuka pengguna (kotak kode dan keluaran, tombol "Run", dan lainnya) dan berkomunikasi dengan front-end playground.
Implementasinya ada dalam berkas
playground.js
dalam repositori go.tools
, yang dapat diimpor dari paket
golang.org/x/tools/godoc/static
.
Beberapa kodenya cukup bersih dan beberapa cukup kasar karena ia adalah hasil
gabungan dari beberapa implementasi dari kode klien.
Fungsi
playground
membaca beberapa elemen HTML dan mengubahnya menjadi widget playground yang
interaktif.
Anda sebaiknya menggunakan fungsi ini jika Anda ingin menempatkan playground
pada situs Anda (lihat Klien lain
di bawah).
Interface
Transport
(tidak secara formal terdefinisi)
mengabstraksi antarmuka pengguna untuk berkomunikasi ke front-end web.
HTTPTransport
adalah sebuah implementasi dari Transport
menggunakan protokol HTTP seperti
yang dijelaskan di atas.
SocketTransport
adalah implementasi menggunakan WebSocket (lihat "Eksekusi luring" di bawah).
Untuk memenuhi
aturan same-origin,
beberapa server web (godoc, misalnya) mengirim permintaan ke /compile
lewat
layanan proxy playground di https://golang.org/compile.
Paket
golang.org/x/tools
membantu melakukan hal ini.
Eksekusi luring
Baik tur Go dan perkakas Present dapat berjalan secara luring. Hal ini bagus untuk yang memiliki koneksi internet yang terbatas atau untuk presentasi yang tidak dapat (dan sebaiknya tidak) bergantung pada koneksi internet yang selalu bekerja baik.
Untuk eksekusi secara luring, perkakas menjalankan versi back-end playground-nya sendiri di mesin lokal. Back-end menggunakan perkakas Go biasa tanpa ada modifikasi yang disebutkan di atas dan menggunakan WebSocket untuk berkomunikasi dengan klien.
Implementasi WebSocket untuk back-end dapat ditemukan dalam paket golang.org/x/tools/playground/socket. Wicara Inside Present mendiskusikan kode ini secara rinci.
Klien lain
Layanan playground digunakan oleh banyak proyek Go ( Go by Example adalah salah satunya) dan kami suka bila Anda menggunakannya pada situs Anda sendiri. Apa yang kami harapkan yaitu supaya Anda kontak kami terlebih dahulu, menggunakan "user-agent" yang unik dalam permintaan Anda (sehingga kita dapat mengidentifikasi Anda), dan layanan Anda menguntungkan komunitas Go.
Kesimpulan
Dari godoc sampai tur sampai artikel ini, playground telah menjadi bagian penting dari sejarah dokumentasi Go. Dengan adanya penambahan sistem berkas dan jaringan palsu kami bergairah untuk mengembangkan materi-materi pembelajaran supaya dapat membahas hal-hal tersebut.
Namun, pada akhirnya, playground itu hanyalah puncak. Dengan dukungan Native Client yang dijadwalkan dalam Go 1.3, kami berharap dapat melihat apa yang komunitas dapat lakukan dengannya.
Artikel ini adalah bagian ke 12 dari Go Advent Calendar sebuah kumpulan artikel blog harian sampai Desember.