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

https://blog

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.

Perubahan dalam scheduler ini dapat ditemukan dalam proc.c dan time.goc.

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 oleh fsysFile,

  • 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 https://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.