Bahasa Go pertama kali diumumkan pada akhir tahun 2009, dan dirilis secara resmi pada tahun 2012, tetapi hanya dalam beberapa tahun terakhir ini mulai mendapatkan pengakuan yang serius. Go adalah salah satu bahasa dengan pertumbuhan tercepat di tahun 2018 dan bahasa pemrograman terpopuler ketiga di tahun 2019 .
Karena bahasa Go sendiri terbilang baru, komunitas pengembang tidak terlalu ketat tentang cara menulis kode. Jika kita melihat konvensi serupa di komunitas bahasa lama, seperti Java, ternyata sebagian besar proyek memiliki struktur yang mirip. Ini bisa sangat berguna saat menulis basis kode besar, namun, banyak yang mungkin berpendapat bahwa ini akan menjadi kontraproduktif dalam konteks praktis modern. Saat kita beralih ke penulisan sistem mikro dan mempertahankan basis kode yang relatif kompak, fleksibilitas Go dalam menyusun proyek menjadi sangat menarik.
Semua orang tahu contoh hello world http di Golang , dan dapat dibandingkan dengan contoh serupa di bahasa lain, misalnya, di Java... Tidak ada perbedaan yang signifikan antara yang pertama dan yang kedua, baik dalam kompleksitas maupun dalam jumlah kode yang perlu ditulis untuk mengimplementasikan contoh. Namun ada perbedaan mendasar dalam pendekatannya. Go mendorong kami untuk " menulis kode sederhana jika memungkinkan ." Selain dari aspek berorientasi objek Java, menurut saya hal terpenting dari cuplikan kode ini adalah: Java memerlukan instance terpisah untuk setiap operasi (instance
HttpServer), sedangkan Go mendorong kita untuk menggunakan global singleton.
Dengan cara ini Anda harus mempertahankan lebih sedikit kode dan melewatkan lebih sedikit tautan di dalamnya. Jika Anda tahu bahwa Anda hanya perlu membuat satu server (dan ini biasanya terjadi), lalu mengapa repot-repot melakukannya? Filosofi ini tampaknya semakin menarik seiring dengan pertumbuhan basis kode Anda. Namun demikian, hidup terkadang memunculkan kejutan :(. Faktanya adalah Anda masih memiliki beberapa tingkat abstraksi untuk dipilih, dan jika Anda menggabungkannya dengan tidak benar, Anda dapat membuat diri Anda jebakan serius.
Itulah mengapa saya ingin menarik perhatian Anda ke tiga pendekatan pengorganisasian dan penataan kode Go. Masing-masing pendekatan ini menyiratkan tingkat abstraksi yang berbeda. Sebagai kesimpulan, saya akan membandingkan ketiganya dan memberi tahu Anda dalam kasus aplikasi mana setiap pendekatan ini paling sesuai.
Kami akan mengimplementasikan server HTTP yang berisi informasi tentang pengguna (dilambangkan sebagai DB Utama pada gambar berikut), di mana setiap pengguna diberi peran (katakanlah dasar, moderator, administrator), dan juga menerapkan database tambahan (pada gambar berikutnya, dilambangkan sebagai Configuration DB), yang menentukan sekumpulan hak akses yang diberikan untuk masing-masing peran (misalnya membaca, menulis, mengedit). Server HTTP kami harus menerapkan titik akhir yang mengembalikan serangkaian hak akses yang dimiliki pengguna dengan ID yang diberikan.
Selanjutnya, mari kita asumsikan bahwa database konfigurasi jarang berubah dan membutuhkan waktu lama untuk memuat, jadi kita akan menyimpannya di RAM, memuatnya ketika server dimulai, dan memperbaruinya setiap jam.
Semua kode ada di repositori untuk artikel ini yang terletak di GitHub.
Pendekatan I: Paket tunggal
Pendekatan paket tunggal menggunakan hierarki saudara di mana seluruh server diimplementasikan dalam satu paket. Semua kodenya .
Peringatan: komentar dalam kode bersifat informatif, penting untuk memahami prinsip setiap pendekatan.
/main.go
package main
import (
"net/http"
)
// ,
// , -,
// , .
var (
userDBInstance userDB
configDBInstance configDB
rolePermissions map[string][]string
)
func main() {
// ,
// ,
// .
//
// , , ,
// .
userDBInstance = &someUserDB{}
configDBInstance = &someConfigDB{}
initPermissions()
http.HandleFunc("/", UserPermissionsByID)
http.ListenAndServe(":8080", nil)
}
// , , .
func initPermissions() {
rolePermissions = configDBInstance.allPermissions()
go func() {
for {
time.Sleep(time.Hour)
rolePermissions = configDBInstance.allPermissions()
}
}()
}
/database.go
package main
// ,
// .
type userDB interface {
userRoleByID(id string) string
}
// `someConfigDB`.
//
// , MongoDB,
// `mongoConfigDB`.
// `mockConfigDB`.
type someUserDB struct {}
func (db *someUserDB) userRoleByID(id string) string {
// ...
}
type configDB interface {
allPermissions() map[string][]string //
}
type someConfigDB struct {}
func (db *someConfigDB) allPermissions() map[string][]string {
//
}
/handler.go
package main
import (
"fmt"
"net/http"
"strings"
)
func UserPermissionsByID(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query()["id"][0]
role := userDBInstance.userRoleByID(id)
permissions := rolePermissions[role]
fmt.Fprint(w, strings.Join(permissions, ", "))
}
Harap diperhatikan: kami masih menggunakan file yang berbeda, ini untuk pemisahan masalah. Ini membuat kode lebih mudah dibaca dan lebih mudah dikelola.
Pendekatan II: Paket Berpasangan
Dalam pendekatan ini, mari pelajari apa itu pengelompokan. Paket harus bertanggung jawab sepenuhnya atas beberapa perilaku tertentu. Di sini kami mengizinkan paket untuk berinteraksi satu sama lain - jadi kami harus menjaga lebih sedikit kode. Namun, kami perlu memastikan bahwa kami tidak melanggar prinsip tanggung jawab tunggal dan oleh karena itu memastikan bahwa setiap bagian logika diterapkan sepenuhnya dalam paket terpisah. Panduan penting lainnya untuk pendekatan ini adalah karena Go tidak mengizinkan dependensi melingkar antar paket, Anda perlu membuat paket netral yang hanya berisi definisi antarmuka kosong dan instance tunggal . Ini akan menghilangkan ketergantungan cincin. Seluruh kode...
/main.go
package main
// : main – ,
// .
import (
"github.com/myproject/config"
"github.com/myproject/database"
"github.com/myproject/definition"
"github.com/myproject/handler"
"net/http"
)
func main() {
// , ,
// , ,
// .
definition.UserDBInstance = &database.SomeUserDB{}
definition.ConfigDBInstance = &database.SomeConfigDB{}
config.InitPermissions()
http.HandleFunc("/", handler.UserPermissionsByID)
http.ListenAndServe(":8080", nil)
}
/definition/database.go
package definition
// , ,
// .
// , ;
// , , ,
// .
var (
UserDBInstance UserDB
ConfigDBInstance ConfigDB
)
type UserDB interface {
UserRoleByID(id string) string
}
type ConfigDB interface {
AllPermissions() map[string][]string //
}
/definition/config.go
package definition
var RolePermissions map[string][]string
/database/user.go
package database
type SomeUserDB struct{}
func (db *SomeUserDB) UserRoleByID(id string) string {
//
}
/database/config.go
package database
type SomeConfigDB struct{}
func (db *SomeConfigDB) AllPermissions() map[string][]string {
//
}
/config/permissions.go
package config
import (
"github.com/myproject/definition"
"time"
)
// ,
// config.
func InitPermissions() {
definition.RolePermissions = definition.ConfigDBInstance.AllPermissions()
go func() {
for {
time.Sleep(time.Hour)
definition.RolePermissions = definition.ConfigDBInstance.AllPermissions()
}
}()
}
/handler/user_permissions_by_id.go
package handler
import (
"fmt"
"github.com/myproject/definition"
"net/http"
"strings"
)
func UserPermissionsByID(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query()["id"][0]
role := definition.UserDBInstance.UserRoleByID(id)
permissions := definition.RolePermissions[role]
fmt.Fprint(w, strings.Join(permissions, ", "))
}
Pendekatan III: Paket independen
Dengan pendekatan ini, proyek juga diatur dalam paket. Dalam hal ini, setiap paket harus mengintegrasikan semua dependensinya secara lokal , melalui antarmuka dan variabel . Jadi, ia sama sekali tidak tahu apa-apa tentang paket lain . Dengan pendekatan ini, paket dengan definisi yang disebutkan dalam pendekatan sebelumnya sebenarnya akan tercoreng di antara semua paket lainnya; setiap paket mendeklarasikan antarmukanya sendiri untuk setiap layanan. Sekilas, ini mungkin tampak seperti duplikasi yang mengganggu, namun kenyataannya tidak. Setiap paket yang menggunakan layanan harus mendeklarasikan antarmukanya sendiri, yang hanya menentukan apa yang dibutuhkan dari layanan ini dan tidak yang lain. Seluruh kode...
/main.go
package main
// : – ,
// .
import (
"github.com/myproject/config"
"github.com/myproject/database"
"github.com/myproject/handler"
"net/http"
)
func main() {
userDB := &database.SomeUserDB{}
configDB := &database.SomeConfigDB{}
permissionStorage := config.NewPermissionStorage(configDB)
h := &handler.UserPermissionsByID{UserDB: userDB, PermissionsStorage: permissionStorage}
http.Handle("/", h)
http.ListenAndServe(":8080", nil)
}
/database/user.go
package database
type SomeUserDB struct{}
func (db *SomeUserDB) UserRoleByID(id string) string {
//
}
/database/config.go
package database
type SomeConfigDB struct{}
func (db *SomeConfigDB) AllPermissions() map[string][]string {
//
}
/config/permissions.go
package config
import (
"time"
)
// , ,
// , ,
// `AllPermissions`.
type PermissionDB interface {
AllPermissions() map[string][]string //
}
// ,
// , , ,
//
type PermissionStorage struct {
permissions map[string][]string
}
func NewPermissionStorage(db PermissionDB) *PermissionStorage {
s := &PermissionStorage{}
s.permissions = db.AllPermissions()
go func() {
for {
time.Sleep(time.Hour)
s.permissions = db.AllPermissions()
}
}()
return s
}
func (s *PermissionStorage) RolePermissions(role string) []string {
return s.permissions[role]
}
/handler/user_permissions_by_id.go
package handler
import (
"fmt"
"net/http"
"strings"
)
//
type UserDB interface {
UserRoleByID(id string) string
}
// ... .
type PermissionStorage interface {
RolePermissions(role string) []string
}
// ,
// , .
type UserPermissionsByID struct {
UserDB UserDB
PermissionsStorage PermissionStorage
}
func (u *UserPermissionsByID) ServeHTTP(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query()["id"][0]
role := u.UserDB.UserRoleByID(id)
permissions := u.PermissionsStorage.RolePermissions(role)
fmt.Fprint(w, strings.Join(permissions, ", "))
}
Itu saja! Kami telah melihat tiga tingkat abstraksi, yang pertama adalah yang paling tipis, berisi status global dan logika yang digabungkan erat, tetapi memberikan implementasi tercepat dan jumlah kode paling sedikit untuk ditulis dan dipelihara. Opsi kedua adalah hibrid sedang, dan yang ketiga sepenuhnya mandiri dan cocok untuk penggunaan berulang, tetapi hadir dengan upaya maksimal dengan dukungan.
Pro dan kontra
Pendekatan I: Paket Tunggal
Untuk
- Lebih sedikit kode, implementasi lebih cepat, lebih sedikit pekerjaan pemeliharaan
- Tidak ada paket, yang berarti Anda tidak perlu khawatir tentang dependensi ring
- Mudah untuk diuji karena tersedia antarmuka layanan. Untuk menguji sepotong logika, Anda dapat menentukan implementasi apa pun pilihan Anda (konkret atau tiruan) untuk singleton, lalu menjalankan logika pengujian.
Melawan
- Paket satu-satunya juga tidak menyediakan akses private, semuanya terbuka dari mana-mana. Akibatnya, tanggung jawab pengembang meningkat. Misalnya, ingat bahwa Anda tidak bisa langsung membuat instance struktur saat fungsi konstruktor diperlukan untuk melakukan beberapa logika inisialisasi.
- Status global (instance tunggal) dapat membuat asumsi yang tidak terpenuhi, misalnya, instance tunggal yang tidak diinisialisasi dapat memicu kepanikan penunjuk null saat waktu proses.
- Karena logikanya terkait erat, tidak ada apa pun dalam proyek ini yang dapat digunakan kembali dengan mudah, dan akan sulit untuk mengekstrak komponen apa pun darinya.
- Ketika Anda tidak memiliki paket yang secara independen mengelola setiap bagian dari logika, pengembang harus sangat berhati-hati dan menempatkan semua potongan kode dengan benar, jika tidak, perilaku yang tidak diharapkan dapat terjadi.
Pendekatan II: Paket Berpasangan
Per
- Saat memaketkan proyek, akan lebih mudah untuk menjamin tanggung jawab atas logika tertentu di dalam paket, dan ini dapat diterapkan menggunakan kompilator. Selain itu, kami akan dapat menggunakan akses pribadi dan mengontrol elemen kode mana yang dibuka untuk kami.
- Menggunakan paket dengan definisi memungkinkan Anda bekerja dengan instance tunggal sambil menghindari dependensi melingkar. Dengan cara ini Anda dapat menulis lebih sedikit kode, menghindari melewatkan referensi saat mengelola instance, dan menghindari membuang waktu untuk masalah yang berpotensi muncul selama kompilasi.
- Pendekatan ini juga kondusif untuk pengujian, karena terdapat antarmuka layanan. Dengan pendekatan ini, pengujian internal setiap paket dimungkinkan.
Melawan
- Ada beberapa biaya tambahan saat mengatur proyek dalam paket - misalnya, implementasi awal harus memakan waktu lebih lama dibandingkan dengan pendekatan paket tunggal.
- Menggunakan status global (contoh tunggal) dengan pendekatan ini juga dapat menyebabkan masalah.
- Proyek ini dibagi menjadi beberapa paket, yang sangat memfasilitasi ekstraksi dan penggunaan kembali elemen individu. Namun, paket tidak sepenuhnya independen karena semuanya berinteraksi dengan paket definisi. Dengan pendekatan ini, ekstraksi kode dan penggunaan kembali tidak sepenuhnya otomatis.
Pendekatan III:
Profesional Independen
- Saat menggunakan paket, kami memastikan bahwa logika spesifik diimplementasikan dalam satu paket dan kami memiliki kendali penuh atas akses.
- Seharusnya tidak ada dependensi melingkar yang potensial karena paketnya benar-benar mandiri.
- Semua paket sangat dapat dipulihkan dan digunakan kembali. Dalam semua kasus ketika kami membutuhkan paket di proyek lain, kami cukup mentransfernya ke ruang bersama dan menggunakannya tanpa mengubah apa pun di dalamnya.
- Jika tidak ada status global, maka tidak ada perilaku yang tidak diinginkan.
- Pendekatan ini paling baik untuk pengujian. Setiap paket dapat diuji sepenuhnya tanpa khawatir bahwa itu mungkin bergantung pada paket lain melalui antarmuka lokal.
Melawan
- Pendekatan ini jauh lebih lambat untuk diterapkan dibandingkan dengan dua pendekatan sebelumnya.
- Lebih banyak kode perlu dipertahankan. Karena tautan sedang diteruskan, banyak tempat harus diperbarui setelah perubahan besar dilakukan. Selain itu, ketika kami memiliki banyak antarmuka yang menyediakan layanan yang sama, kami harus memperbarui antarmuka tersebut setiap kali kami membuat perubahan pada layanan itu.
Kesimpulan dan contoh penggunaan
Mengingat kurangnya pedoman untuk menulis kode di Go, dibutuhkan banyak bentuk dan bentuk yang berbeda, dan setiap opsi memiliki kelebihan yang menarik. Namun, mencampur pola desain yang berbeda dapat menimbulkan masalah. Untuk memberi Anda gambaran tentang mereka, saya telah membahas tiga pendekatan berbeda untuk menulis dan menyusun kode Go.
Jadi kapan setiap pendekatan harus digunakan? Saya menyarankan pengaturan ini:
Pendekatan I : Pendekatan paket tunggal mungkin paling tepat ketika bekerja dalam tim kecil yang sangat berpengalaman dalam proyek-proyek kecil yang membutuhkan hasil yang cepat. Pendekatan ini lebih sederhana dan lebih dapat diandalkan untuk memulai dengan cepat, meskipun memerlukan perhatian dan koordinasi yang serius pada tahap dukungan proyek.
Pendekatan II: Pendekatan paket berpasangan dapat disebut sintesis hibrid dari dua pendekatan lainnya: di antara keuntungannya adalah permulaan yang relatif cepat dan kemudahan dukungan, sementara pada saat yang sama, hal ini menciptakan kondisi untuk kepatuhan yang ketat terhadap aturan. Ini sesuai untuk proyek yang relatif besar dan tim besar, tetapi memiliki kegunaan ulang kode yang terbatas dan ada kesulitan tertentu dalam pemeliharaannya.
Pendekatan III : Pendekatan paket independen paling sesuai untuk proyek yang kompleks, berjangka panjang, dikembangkan oleh tim besar, dan untuk proyek yang memiliki potongan logika yang dibuat dengan tujuan untuk digunakan kembali lebih lanjut. Pendekatan ini membutuhkan waktu lama untuk diterapkan dan sulit dipertahankan.