- Layanan HTTP Go yang dipelihara dalam jangka panjang akan lebih mudah dirawat dan diverifikasi jika disusun dengan injeksi dependensi eksplisit, rute yang dikumpulkan di satu tempat, dan fungsi
runyang mudah diuji - Alih-alih metode pada struct server, handler dibuat sebagai fungsi yang mengembalikan
http.Handleryang menerima nilai yang dibutuhkan lewat closure, dan middleware bersama dikombinasikan saat pembuatan server serta pendaftaran rute - Jaga
func main()tetap tipis, lalu injeksikancontext.Context, argumen, akses environment, dan standar input/output kerun()agar penanganan shutdown dan kontrol pengujian menjadi sederhana - Helper untuk encoding/decoding request/response, validasi, adaptor middleware, dan inisialisasi tertunda
sync.Oncemengurangi kode berulang sambil tetap mempertahankan alur standar Gonet/http - Untuk pengujian, pendekatan end-to-end yang mendekati pemanggilan API nyata lebih disukai daripada pengujian handler satu per satu, dengan setiap test menjalankan servernya sendiri dan memeriksa kesiapan lewat
/healthzatau/readyz
Pembuatan server dan titik masuk layanan
- Konstruktor
NewServerdijadikan fungsi yang membuathttp.Handlerinti layanan- Biasanya ada satu untuk tiap layanan, dan rute internal akan meneruskan request ke masing-masing handler
- Semua dependensi seperti logger, konfigurasi, storage, dan klien eksternal diterima sebagai argumen
- Sebisa mungkin mengembalikan
http.Handler, meski pada kasus kompleks tipe khusus juga bisa dipakai - Setelah menyusun muxer sendiri, hasilnya diteruskan ke fungsi pendaftaran rute di
routes.go
- Penanganan HTTP yang dibutuhkan bersama oleh semua endpoint digabungkan di
NewServer- CORS
- middleware autentikasi
- logging
- middleware trace ID
- Meski daftar argumen dependensi menjadi panjang, pendekatan argumen fungsi tetap lebih disukai
- Jika field struct terlewat, compiler belum tentu bisa mencegahnya, tetapi argumen fungsi membuat pemanggilan mustahil dilakukan bila nilai yang dibutuhkan tidak diberikan
- Daftar argumen yang panjang tetap mudah dibaca jika diformat vertikal
- Dependensi yang tidak dipakai pada test tertentu bisa diisi
nilsebagai sinyal bahwa ia memang tidak digunakan
Mengumpulkan permukaan API di routes.go
routes.godijadikan file tempat semua rute layanan bisa dilihat di satu lokasi- Setiap proyek punya satu tempat untuk meninjau permukaan API
- Karena
NewServerpunya daftar dependensi besar,addRoutesjuga bisa memiliki daftar argumen serupa - Pemeriksaan tipe Go akan menangkap argumen yang hilang atau urutannya salah
- Sebisa mungkin,
addRoutesdijaga sederhana dan datar- Pekerjaan yang bisa menghasilkan error ditangani lebih dulu di fungsi
run - Pada tahap pendaftaran handler, fokusnya adalah routing seperti
mux.Handle,mux.HandleFunc, danhttp.NotFoundHandler - Jika desainnya mengharuskan handler sendiri mengembalikan error, maka
addRoutesjuga dapat mengembalikan error
- Pekerjaan yang bisa menghasilkan error ditangani lebih dulu di fungsi
main cukup memanggil run
func main()dijadikan fungsi tipis yang memanggilrun(), lalu jika ada error menulis kestderrdan keluar secara tidak normalrunmenerima argumen seperticontext.Context, argumen program, input/output, dan fungsi akses environment sebagai elemen dasar sistem operasi- Karena
runmengembalikan error, penanganannya bisa dilakukan seperti kode Go biasa
- Contoh nilai yang dapat diberikan ke
run:os.Args: untuk argumen eksekusi program dan parsing flagos.Stdin: membaca inputos.Stdout: menulis outputos.Stderr: menulis log erroros.Getenv: membaca variabel environmentos.Getwd: mendapatkan direktori kerja saat ini
signal.NotifyContextdikonfigurasi di dalamrun- Saat sinyal penghentian seperti
Ctrl+Cmasuk, context akan dibatalkan - Jika
runmengembalikannil, program berakhir normal - Jika mengembalikan error,
mainakan menampilkan error dan keluar dengan kode non-zero
- Saat sinyal penghentian seperti
- Dengan menghindari state global, lebih banyak test bisa memakai
t.Parallel()- Pemanggilan
runberkali-kali tidak saling mengganggu - Flag diproses dengan
flags.NewFlagSetdi dalamrun, bukanflagglobal - Variabel environment dikendalikan dengan menyuntikkan
getenv func(string) stringalih-alih mengubah environment nyata - Berbeda dengan
t.SetEnv, pendekatan ini memungkinkan test paralel tetap digunakan
- Pemanggilan
Penanganan shutdown dan status kesiapan
- Context harus diteruskan ke seluruh lapisan layanan
- Saat sinyal shutdown datang, context dibatalkan
- Pekerjaan panjang atau berulang harus memeriksa
ctx.Err()atauctx.Done()lalu berhenti - Jika goroutine lain dijalankan, context juga dipakai untuk menentukan kapan harus berhenti
- Saat shutdown, server HTTP dihentikan secara elegan dengan memanggil
Shutdown- Dalam contoh, ada goroutine terpisah yang menunggu
ctx.Done() - Context shutdown diberi timeout
10 * time.Second - Jika terjadi error saat shutdown, error dicatat ke
stderr
- Dalam contoh, ada goroutine terpisah yang menunggu
- Untuk memastikan server benar-benar siap di test, sediakan endpoint
/healthzatau/readyz- Sinyal siap bisa juga dibuat lewat channel terpisah, tetapi pemeriksaan lewat request HTTP nyata lebih disukai
- Loop pemeriksaan kesiapan terus melakukan request sampai mendapat
200 OK - Jika context dibatalkan atau timeout tercapai, fungsi mengembalikan error
- Dalam contoh, loop tidur
250msdi antara request
Cara menyusun handler
- Fungsi handler mengembalikan
http.Handleratauhttp.HandlerFunc, bukan langsung mengimplementasikannya- Contoh:
func handleSomething(logger *Logger) http.Handler - Ini memungkinkan dibuatnya environment closure per handler
- Nilai yang sudah diinisialisasi bisa dipakai saat menangani request
- Contoh:
- Data bersama aman jika hanya digunakan sebagai baca-saja
- Jika handler memodifikasi nilainya, dibutuhkan perlindungan seperti mutex
- Menyimpan state program di closure umumnya tidak disarankan
- Di lingkungan cloud, sulit mengasumsikan instance akan hidup lama
- Server bisa dimatikan untuk menghemat resource, atau crash karena alasan lain
- Banyak instance bisa berjalan bersamaan dan request dapat didistribusikan dengan cara yang sulit diprediksi
- State persisten pada proyek nyata lebih baik disimpan di database atau API storage terpisah
Encoding/decode request/response dan validasi
- Karena semua layanan membutuhkan decoding body request dan encoding body response, sediakan helper
encode/decode- Contohnya menetapkan JSON
Content-Type, menulis status code, lalu memanggiljson.NewEncoder(w).Encode(v) - Decoding membungkus
json.NewDecoder(r.Body).Decode(&v)dan menambahkan konteks pada error - Dengan generics, inferensi tipe bisa dipakai seperti
encode(w, r, http.StatusOK, obj) - Karena
decodemengembalikan tipe, tipe yang diharapkan harus disebutkan, misalnyadecode[CreateSomethingRequest](https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/r)
- Contohnya menetapkan JSON
- Untuk validasi, digunakan interface dengan satu metode
- Interface
ValidatorberbentukValid(ctx context.Context) map[string]string - Jika tidak ada masalah, akan dikembalikan map dengan panjang 0
- Untuk field yang bermasalah, nama field menjadi key dan penjelasan yang mudah dipahami manusia menjadi value
- Interface
- Target validasi cocok untuk pemeriksaan field yang cepat
- apakah field wajib kosong atau tidak
- apakah format string tertentu seperti email benar
- apakah angka berada dalam rentang yang diizinkan
- Pemeriksaan yang lebih kompleks seperti query database ditangani di tempat lain
- Pemeriksaan seperti itu terlalu penting untuk disembunyikan di fungsi validasi cepat
- Versi generik
decodeValid[T Validator]memaksa tipeTuntuk benar-benar mengimplementasikanValidator - Memanggil
len(problems)pada mapniltetap menghasilkan 0, jadi tidak menimbulkan panic
Pola adaptor middleware
- Middleware menerima
http.Handlerlalu mengembalikanhttp.Handlerbaru- Ia bisa menjalankan kode sebelum dan sesudah memanggil handler asli
- Bergantung pada kondisi, ia juga bisa memilih untuk tidak memanggil handler asli
- Dalam contoh,
adminOnlymengembalikanHTTP 404 Not Foundbila bukan admin dan tidak meneruskan ke handler asli
- Lokasi penerapan middleware biasanya ditempatkan di
routes.go- Dengan melihat daftar endpoint saja, sudah bisa diketahui middleware apa yang melekat pada tiap rute
- Jika daftar middleware panjang, dapat dipecah menjadi beberapa baris agar lebih mudah dibaca
- Middleware dengan banyak dependensi dibungkus dalam fungsi yang mengembalikan middleware
newMiddleware(logger, db, slackClient, rroll)mengembalikanfunc(http.Handler) http.Handler- Dalam kode pendaftaran rute, ia bisa dipakai secara ringkas seperti
middleware(handleSomething(...)) - Bisa juga didefinisikan
type middleware func(h http.Handler) http.Handler, tetapi menulis tipe kembalian secara langsung lebih jelas saat dibaca
Memperkecil cakupan tipe request/response
- Tipe request/response yang hanya dipakai oleh endpoint tertentu bisa didefinisikan di dalam fungsi handler
- Namespace global menjadi lebih bersih
- Ini mencegah handler lain bergantung pada tipe yang tidak dijamin stabil
- Dalam kode test, kebutuhan terhadap tipe yang sama bisa menimbulkan gesekan
- Dalam kasus seperti itu, memindahkan tipe ke luar juga masuk akal
- Jika tipe request/response berada di dalam handler, test bisa mendeklarasikan struct anonim baru atau tipe lokal sendiri
- Tipe lokal di test membantu menunjukkan niat
- Misalnya bila endpoint
/greethanya membutuhkan fieldName, bukan seluruhPerson, maka struct input di test cukup memilikiName - Pembaca test langsung paham field mana yang benar-benar penting bagi endpoint tersebut
- Misalnya bila endpoint
Menunda inisialisasi dengan sync.Once
- Pekerjaan mahal saat persiapan handler dapat ditunda sampai request pertama dengan
sync.Once- Waktu startup aplikasi menjadi lebih singkat
- Jika handler tidak pernah dipanggil, pekerjaan mahal itu tidak akan dijalankan
- Dalam contoh, parsing file template dilakukan hanya sekali pada request pertama
sync.Oncemenjamin kode hanya dieksekusi satu kali- Request lain yang datang bersamaan akan menunggu sampai inisialisasi selesai
- Pemeriksaan error dilakukan di luar
init.Doagar error tetap terus dimunculkan
- Pendekatan ini memindahkan waktu inisialisasi dari startup ke akses pertama endpoint saat runtime
- Pada lingkungan yang banyak memakai Google App Engine, pendekatan ini bisa cocok
- Tergantung lingkungan deployment, perlu diputuskan di mana dan kapan
sync.Oncesebaiknya digunakan
Strategi pengujian
- Struktur ini menjadikan kemudahan pengujian sebagai tujuan penting
- Fungsi
runmemungkinkan program dijalankan langsung dari kode test - Test dinilai dari seberapa mudah ia membantu memahami perilaku program, mengurangi kekhawatiran saat perubahan, dan memberi kepercayaan saat lulus sebelum deploy ke produksi
- Fungsi
- Handler saja tetap bisa diuji secara terpisah
- Panggil fungsi pembuat handler dan berikan dependensi yang diperlukan
- Gunakan
httptest.NewRecorderdanhttp.NewRequestuntuk menyusun request dan response - Verifikasi status code, body response, dan header
- Pendekatan ini melewati middleware seperti autentikasi dan langsung masuk ke kode handler
- Pendekatan yang lebih disukai adalah yang mendekati pengujian end-to-end
- Panggil
runagar program berjalan mendekati cara eksekusi sebenarnya - Ini mencakup parsing argumen, penyambungan dependensi, migrasi database, sampai startup server
- Saat test memanggil API, semua lapisan termasuk
routes.goikut tervalidasi - Ia juga bisa berinteraksi dengan database nyata
- Panggil
- Pendekatan ini membantu mengurangi pengujian yang berulang
- Jika semua lapisan diuji terpisah, hal yang sama bisa diverifikasi berkali-kali dengan cara yang sedikit berbeda
- Test end-to-end menyediakan himpunan test utama yang menjelaskan interaksi antara pengguna dan sistem
- Unit test yang sudah ada dari TDD dan sejenisnya bisa dipertahankan bila masih relevan, tetapi bisa dihapus bila hanya mengulang isi yang sama dengan test end-to-end
- Setiap test dapat menjalankan instance programnya sendiri
- Tiap test bisa memberikan argumen, flag, standar input/output, dan variabel environment yang berbeda
- Buat fungsi cancel dengan
context.WithCancellalu daftarkan ket.Cleanup(cancel) - Saat test selesai, context dibatalkan dan program berhenti secara elegan
t.Cleanupdi Go 1.14 digunakan sebagai alternatif dibanding memakaidefersecara langsung
Cakupan penerapan nyata dan konteks organisasi
- Saat membuat API sederhana, pola ini menargetkan kode yang mudah dibaca dan mudah dikembangkan
- Mudah disalin lalu diperluas sebagai pola
- Mudah dikerjakan oleh orang baru
- Mengurangi rasa cemas saat melakukan perubahan
- Disusun secara eksplisit tanpa perilaku yang terasa seperti sihir
- Bahkan jika memakai alat pembangkit kode, pendekatan ini tetap bisa dipertahankan
- Sebagai contoh, Oto package dapat dipakai untuk menghasilkan boilerplate berbasis template
- Pada proyek besar atau organisasi besar, pilihan teknologi yang sudah ada bisa mengubah keputusan
- Di organisasi seperti Grafana Labs, alat dan abstraksi tertentu mungkin sudah banyak digunakan
- gRPC adalah salah satu contohnya
- Jika pola dan pengalaman sudah mapan, mengikuti arus tersebut menjadi pilihan yang praktis
- Konteks rangkaian produk Grafana IRM juga dibahas
- Grafana IRM adalah rangkaian produk yang sedang dibangun di Grafana Labs
- Grafana Alerting mengirim notifikasi saat metrik keluar dari batas yang diizinkan
- Grafana OnCall mengotomatisasi proses menghubungi orang yang tepat melalui jadwal dan aturan eskalasi
- Grafana Incident membuat ruang Zoom, channel Slack khusus, dan timeline kejadian untuk membantu respons insiden
- Item yang diberi reaksi emoji wajah robot di channel Slack akan ditambahkan ke timeline
1 komentar
Komentar Hacker News
Saya pernah mencoba pendekatan dengan validator terpisah seperti metode
Valid, tetapi setelah membaca “Parse, Don’t Validate” [0] dari Lexi Lambda, saya merasa memanfaatkan type checker Go jauh lebih sedikit menimbulkan kesalahanMisalnya, jika ingin memastikan pengguna sama sekali tidak bisa menetapkan nama pengguna ilegal yang mengandung kurung sudut, dengan pendekatan validator Anda harus memanggil validator di setiap jalur kode tempat nama pengguna berasal dari input yang tidak tepercaya
Sebaliknya, jika Anda menyediakan tipe
Usernamedan konstruktorNewUsername(username string) (Username, error), keberadaan objekUsernamesaja sudah memberi jaminan bahwa validasi telah dilewati[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
string; parse dulu lalu beri tipePola ini memungkinkan validasi dilakukan kapan saja, baik lebih awal maupun lebih lambat, tetapi tidak memberi tahu kapan sebaiknya dilakukan. Biasanya sering kali paling baik dilakukan sebagai bagian dari proses parsing/validasi objek yang lebih besar
Untuk gagasan menangani data yang belum divalidasi dalam konteks UI, “I is for Intent” [1] dari Steven Witten bisa menjadi rujukan
[1] https://acko.net/blog/i-is-for-intent/
Usernamedisertakan di dalam struct dan Anda lupa menetapkan nilainya, yang masuk adalah zero value yang bisa melanggar constraintSalah satu pola yang paling tidak saya sukai adalah menerima objek Config yang mewakili konfigurasi seluruh sistem lalu mengopernya ke mana-mana secara mutable
Dengan begitu, semuanya menjadi terkopel melalui objek konfigurasi. Di salah satu sistem, seseorang menulis kembali nilai ke objek konfigurasi yang diterimanya, sehingga agar berjalan benar setiap bagian harus dikonfigurasi dalam urutan tertentu
Dalam kasus lain, sebuah subsistem menulis data ke objek konfigurasi yang nantinya akan dibaca, sehingga sebagian sistem tidak bisa dinonaktifkan
Pola “konfigurasi sebagai satu nilai mutable besar” adalah pola yang cukup merepotkan, bukan hanya di Go tetapi juga di bahasa lain
Dalam proyek Python, saya sering memakai
dataclasskonfigurasi yang immutable dan mengopernya ke beberapa modul. Ketika beberapa fungsi bergantung pada beberapa nilai, alih-alih mengoper masing-masing sebagai argumen fungsi dan mendefinisikan tipenya terpisah, semua variabel dan definisi tipe berada di satudataclass, sehingga ini menjadi pola desain yang cukup praktisOptionoptionsinternal hanya diubah saat pembuatan, dan ke luar hanya mengeksposConfigbeserta accessor. Misalnya bisa dibuat seperticonfig.New(config.Name("Emanon"))lalu dibaca dengancfg.Name()Configuntuk tiap package, dan menjadikanconfigs.Configsebagai bentuk yang mengumpulkanConfigdari tiap packageIni mungkin bukan best practice Go, tetapi saat startup seluruh konfigurasi sistem bisa dibuat sebagai satu entitas, dan ke tiap package cukup dioper dependensi minimum yang dibutuhkan, jadi enak
Saat testing juga sedikit lebih mudah karena tidak perlu membuat konfigurasi seluruh sistem palsu hanya untuk mengetes satu package
Jangan pernah mengubahnya. Karena tidak bisa tahu dipakai di mana dan bagaimana, biaya tenaga kerja yang terbuang jadi menyebalkan, dan jika perlu perubahan, lebih baik membuat nilai turunan dari nilai asli
Lucunya, secara desain objek konfigurasi itu sampai batas tertentu sudah immutable, dan untuk mengubahnya harus memakai API
WARNING_DO_NOT_USE, tetapi saya tetap mengubah objek dengan itu dan menyebabkan insidenSaya sangat menyukai karya Mat Ryer, dan sejak itu saya telah menerapkan sebagian besar ide dalam versi 2018 tulisan ini ke semua proyek Go saya
Namun, saya selalu merasa kurang nyaman dengan bagian bahwa
NewServeradalah konstruktor besar yang menerima semua dependensi sebagai argumen, dan dalam pengujian kita meneruskannilsebagai sinyal bahwa dependensi yang tidak diperlukan tidak digunakanAkibatnya, bagian besar kode jadi memiliki banyak shared state yang tidak perlu. Padahal dalam praktiknya ada banyak handler HTTP yang hanya perlu memastikan apakah pengguna yang membuat request boleh mengakses resource, lalu memanggil satu fungsi di data store, tetapi ia menjadi bagian dari gumpalan besar yang bisa mengakses semua objek yang dimiliki server induk dan seluruh data store
Meski hanya ingin melakukan mock pada dua method untuk diuji, menulis pengujian sederhana jadi sulit; pola Mat Ryer adalah yang terbaik yang pernah saya lihat sejauh ini, tetapi tetap ada perasaan bahwa seharusnya ada solusi yang lebih baik
Jika memungkinkan, plugin adalah strategi batas kode yang baik. Arsitektur plugin pada dasarnya berada dalam kondisi opt-out, dan baru terlihat ketika dipilih secara eksplisit, sehingga tidak memaksakan semua kemungkinan ke suatu bongkahan kode
Saya menyebut perangkat lunak dengan sifat seperti ini sebagai “à la carte”. Secara umum, kita harus menghindari situasi “melakukan semuanya demi melakukan apa pun”
usersatau packagecommentsPackage-package ini sama sekali tidak memiliki antarmuka HTTP, tetapi masing-masing punya
mainsendiri dan semacam antarmuka CLI. Komentar file//go:build ignoreberguna untuk ituAlih-alih mendefinisikan handler seperti
func HandleX(w http.ResponseWriter, req *http.Request), saya membuatnya menerima dependensi yang diperlukan dalam bentukfunc HandleX(store *DataStore, dep1 Foo, dep2 Bar, commonDep Common) http.HandlerFunc, lalu mengembalikanhttp.HandlerFuncyang sebenarnya di dalamnyaLalu inisialisasi dilakukan sekali di entry point
NewServermelakukan terlalu banyak hal. Kemungkinan terlalu banyak tipe data dan perilaku yang digabungkanContoh sederhana: jika logger ditambahkan sebagai dependensi konstruktor, objek tersebut melakukan sedikit lebih banyak hal daripada implementasi awal yang sederhana. Itu sendiri tidak apa-apa, tetapi sayang jika tidak bisa menemukan cara melakukan logging tanpa mengubah implementasi dari hal yang sederhana itu
Higher-order function, misalnya decorator logger, memungkinkan komposisi, meskipun itu juga punya kekurangan. Tetap saja, itu adalah salah satu bentuk struktur yang bisa dikelola, bukan sebuah kesalahan
Intinya adalah memvalidasi nilai-nilai struct konfigurasi opsional di dalam
NewServer, lalu menyalinnya ke struct server. Dengan begitu, pengujian menjadi jauh lebih mudah karena hanya perlu melakukan mock pada lebih sedikit dependensiSaya juga banyak mencoba functional options pattern seperti yang disarankan banyak orang, tetapi akhirnya meninggalkannya. Rasanya sedikit terlalu cerdik sehingga sulit dibaca, dan menurut saya boilerplate-nya juga lebih banyak dibanding pola struct konfigurasi + validasi lalu salin
[0] https://news.ycombinator.com/item?id=39320170
Saya berharap pemikiran ini diterima lebih luas untuk layanan HTTP dalam bahasa apa pun: jika handler membutuhkan dependensi, ia harus memintanya langsung sebagai argumen, bukan ditempatkan sebagai method yang menggantung pada struct server sehingga saat diuji muncul dependensi mengejutkan
Handler pada layanan HTTP biasanya berisi banyak business logic, dan logika itu kemungkinan memiliki banyak dependensi. Dalam praktiknya, saya sering melihat satu handler memakai DB, cache, blob storage, pemeriksaan otorisasi khusus endpoint, license checker, queue, logger khusus, client metrik, dan sebagainya
Parameternya bisa menjadi 9 atau lebih, dan linter atau aturan praktis biasanya berusaha mencegah ini, tetapi dependensinya tidak hilang; hanya disembunyikan di class/struct
server, lalu kita menipu diri bahwa dependensinya sedikit karena signature method-nya pendekSeiring waktu, saya merasa kode yang menampilkan semua dependensi di signature fungsi/method lebih baik, sekalipun jumlahnya menjadi 20. Dengan begitu kita tidak menipu diri soal meningkatnya kompleksitas kode
Misalnya, dalam struct
CreateUser, saya hanya memasukkan dependensi yang diperlukan untuk pekerjaan itu sepertistore,cache,logger,pub, lalu mengimplementasikanServeHTTPDi
main.goatau tempat pengaturan dependensi, setiap operasi dibuat dan hanya dependensi yang diperlukan yang diteruskan. Dengan cara ini, helper method untuk operasi/handler tertentu bisa tetap dijaga sebagai private method pada struct tersebut, dan itu bagusNamun, jika suatu operasi membutuhkan operasi lain, ini bisa merepotkan karena mulai saling meneruskan atau harus diekstrak menjadi package/service terpisah
Misalnya, dengan
handleHello({ db, cache, blobStore, authz }, req, res), jika dua handler memakai konteks yang persis sama, itu bisa digunakan ulang, dan di titik pemanggilan juga mudah mendeklarasikan konteks per handlerSaya setuju dengan banyak bagian tulisan ini dan ingin menambahkan beberapa hal
Jika WaitGroup diteruskan ke struct service bersama app context, interrupt dapat memicu penghentian aplikasi melalui context, dan goroutine utama dapat menunggu WaitGroup sebelum benar-benar keluar
Untuk program CLI, menguji stdout, stdin, stderr, args, env, dan sebagainya memang berguna, tetapi menurut saya tidak terlalu demikian untuk server HTTP. Saya akan meneruskan konfigurasi terstruktur ke fungsi
runagar pengujian lebih terfokusSaya tidak setuju dengan cara mem-parsing template di handler menggunakan
sync.Once. Menurut saya handler tidak seharusnya melakukan parsing template; itu harus dilakukan saat aplikasi mulai. Jika template tidak bisa di-parse, aplikasi tidak boleh berada dalam status siap menerima request dan harus berakhir dengan exit code non-zeroBelakangan ini sedang mencoba-coba ogen: https://github.com/ogen-go/ogen
Kalau menulis definisi OpenAPI, ia menangani routing, definisi struct, validasi skema JSON, dan sebagainya. Yang perlu saya lakukan hanya mengimplementasikan servicenya
Hal seperti validasi rentang bilangan bulat pada query string itu sangat membosankan, dan kalau ditulis sendiri terlalu mudah membuat typo
Karena masih tahap mencoba-coba, saya belum menemukan sisi buruknya
Menulis IDL serupa seperti Protobuf atau capnproto terasa jauh lebih produktif
[1] https://github.com/danielgtaylor/huma
[2] https://github.com/swaggest/rest
Saya kira menulis spesifikasinya akan membosankan, tetapi ternyata jauh lebih oke daripada dugaan, dan karena pada akhirnya spesifikasi tetap dibutuhkan, menurut saya lebih baik menuliskannya di awal
Saya merasa fx(https://github.com/uber-go/fx) adalah alat yang sangat sederhana sekaligus serbaguna untuk merancang aplikasi
Saran dalam artikel itu masih berguna, tetapi fx sepenuhnya menghilangkan bagian “bagaimana memastikan X sudah diinisialisasi saat Y membutuhkannya”. Masalah N*M berkurang menjadi masalah N; kita hanya perlu memikirkan cara menginisialisasi tiap bagian, tanpa perlu mengkhawatirkan sinkronisasi inisialisasi di antara bagian-bagian itu
Saya sudah cukup banyak memakai library dependency injection di berbagai bahasa dan juga pernah mengimplementasikannya sendiri, tetapi sejauh ini kesederhanaan dan kegunaan umum fx adalah yang paling saya sukai
Dalam sistem yang dirancang dengan baik, masalah ini seharusnya sepele. Memastikan sesuatu sudah diinisialisasi saat ingin memakainya hanyalah soal apakah ia sudah siap untuk diteruskan sebagai argumen konstruktor
Cukup buat seperti
stockService := NewStockService(),orderService := NewOrderService(),orderProcessor := NewOrderProcessor(stockService, orderService)Tidak seharusnya perlu ada “sinkronisasi” inisialisasi, dan kalau salah, kodenya tidak akan terkompilasi. Jika menambahkan dependency melingkar pun, itu akan terlihat jelas karena tidak bisa disusun dalam urutan yang benar
Artikel yang bagus dengan banyak ide menarik. Sulit dipercaya saya tidak tahu soal
signal.NotifyContextSekarang saya bisa mengingat cara menangani signal tanpa perlu copy-paste di setiap proyek
Saya cukup suka pendekatan yang dipakai di sini, tetapi pengujian saya sedikit berbeda
Di
newTestServer(), saya menjalankan server dengan dependency palsu, dan kalau ingin menguji error dependency, saya mengganti properti terkait dengan fake yang mengembalikan errorDengan begitu saya bisa memverifikasi jalur error, entri log, penerbitan metrik, timeout, hingga graceful shutdown
Setelah server mulai berjalan, saya memeriksa port mana yang di-bind. Karena nilai defaultnya
:0, saya harus menunggu port yang benar-benar dialokasikanPengujian “unit” bisa dilakukan di level handler atau level HTTP, dan kode bisa diuji cukup sesuai cara yang akan dilihat pengguna, baik dengan melewati seluruh middleware maupun tanpa melewatinya sama sekali. Menjalankan N instance dan mengujinya secara paralel juga dimungkinkan
Saya tidak memakai Go, tetapi saya suka pola-pola seperti ini. Rasanya cukup universal untuk kode yang dapat diuji
Saya tidak ingin lagi melihat panduan quick start, terutama untuk Python, yang memperlakukan dependency secara implisit/statis/tidak dapat diuji