Pertanyaan pertama untuk developer yang baru mulai menggunakan Go sering kali terlihat seperti ini: "Framework apa yang harus digunakan untuk menyelesaikan masalah X". Meskipun ini adalah pertanyaan yang sangat normal ketika ditanya dengan aplikasi web dan server yang ditulis dalam banyak bahasa lain, dalam kasus Go, ada banyak seluk-beluk yang perlu dipertimbangkan saat menjawab pertanyaan ini. Ada argumen kuat yang mendukung dan menentang penggunaan kerangka kerja dalam proyek Go. Saat mengerjakan artikel dari seri ini, saya melihat tujuan saya sebagai studi yang objektif dan serbaguna tentang masalah ini.
Sebuah tugas
Untuk memulainya, saya ingin mengatakan bahwa di sini saya melanjutkan dari asumsi bahwa pembaca sudah familiar dengan konsep "REST server". Jika Anda membutuhkan penyegar, lihatlah materi yang bagus ini (tapi ada banyak artikel serupa lainnya). Mulai sekarang saya akan berasumsi bahwa Anda akan mengerti apa yang saya maksud ketika saya menggunakan istilah "path", "HTTP header", "response code" dan sejenisnya.
Dalam kasus kami, server adalah sistem backend sederhana untuk aplikasi yang mengimplementasikan fungsi manajemen tugas (seperti Google Keep, Todoist, dan sejenisnya). Server menyediakan REST API berikut untuk klien:
POST /task/ : ID GET /task/<taskid> : ID GET /task/ : DELETE /task/<taskid> : ID GET /tag/<tagname> : GET /due/<yy>/<mm>/<dd> : ,
Perhatikan bahwa API ini dibuat khusus untuk contoh kami. Dalam angsuran berikutnya dari seri ini, kita akan berbicara tentang pendekatan yang lebih terstruktur dan standar untuk desain API.
Server kami mendukung permintaan GET, POST dan DELETE, beberapa di antaranya dengan kemampuan untuk menggunakan banyak jalur. Apa yang ditampilkan dalam tanda kurung sudut (
<...>
) dalam deskripsi API menunjukkan parameter yang diberikan klien ke server sebagai bagian dari permintaan. Misalnya, permintaan
GET /task/42
diarahkan untuk menerima tugas dari server dengan
ID
42
.
ID
Apakah pengenal tugas unik.
Data dikodekan dalam format JSON. Saat menjalankan permintaan
POST /task/
klien mengirimkan representasi JSON dari tugas yang akan dibuat ke server. Dan, demikian pula, respons terhadap permintaan tersebut, yang deskripsinya mengatakan bahwa mereka "mengembalikan" sesuatu, berisi data JSON. Secara khusus, mereka ditempatkan di badan respons HTTP.
Kode
Selanjutnya, kita akan membahas penulisan kode server di Go selangkah demi selangkah. Versi lengkapnya dapat ditemukan di sini . Ini adalah modul Go mandiri yang tidak menggunakan dependensi. Setelah mengkloning atau menyalin direktori proyek ke komputer, server dapat segera, tanpa menginstal apa pun, menjalankan:
$ SERVERPORT=4112 go run .
Harap dicatat bahwa
SERVERPORT
Anda dapat menggunakan port apa pun yang akan mendengarkan di server lokal sambil menunggu koneksi. Setelah server dimulai, menggunakan jendela terminal terpisah, Anda dapat bekerja dengannya menggunakan, misalnya, utilitas
curl
. Anda juga dapat berinteraksi dengannya menggunakan beberapa program serupa lainnya. Contoh perintah yang digunakan untuk mengirim permintaan ke server dapat ditemukan di skrip ini . Direktori yang berisi skrip ini berisi alat untuk pengujian server otomatis.
Model
Mari kita mulai dengan membahas model (atau "lapisan data") untuk server kita. Anda dapat menemukannya di paket
taskstore
(
internal/taskstore
di direktori proyek). Ini adalah abstraksi sederhana yang mewakili database yang menyimpan tugas. Ini API-nya:
func New() *TaskStore
// CreateTask .
func (ts *TaskStore) CreateTask(text string, tags []string, due time.Time) int
// GetTask ID. ID -
// .
func (ts *TaskStore) GetTask(id int) (Task, error)
// DeleteTask ID. ID -
// .
func (ts *TaskStore) DeleteTask(id int) error
// DeleteAllTasks .
func (ts *TaskStore) DeleteAllTasks() error
// GetAllTasks .
func (ts *TaskStore) GetAllTasks() []Task
// GetTasksByTag , ,
// .
func (ts *TaskStore) GetTasksByTag(tag string) []Task
// GetTasksByDueDate , , ,
// .
func (ts *TaskStore) GetTasksByDueDate(year int, month time.Month, day int) []Task
Berikut adalah deklarasi tipe
Task
:
type Task struct {
Id int `json:"id"`
Text string `json:"text"`
Tags []string `json:"tags"`
Due time.Time `json:"due"`
}
Paket
taskstore
mengimplementasikan API ini menggunakan kamus sederhana
map[int]Task
dan menyimpan data dalam memori. Namun tidak sulit untuk membayangkan implementasi API ini yang digerakkan oleh database. Dalam aplikasi nyata
TaskStore
, kemungkinan besar itu adalah antarmuka yang dapat diimplementasikan oleh backend yang berbeda. Tetapi untuk contoh sederhana kami, API ini sudah cukup. Jika Anda ingin berlatih, terapkan
TaskStore
menggunakan sesuatu seperti MongoDB.
Mempersiapkan server untuk bekerja
Fungsi
main
server kami cukup sederhana:
func main() {
mux := http.NewServeMux()
server := NewTaskServer()
mux.HandleFunc("/task/", server.taskHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)
log.Fatal(http.ListenAndServe("localhost:"+os.Getenv("SERVERPORT"), mux))
}
Mari luangkan waktu untuk tim
NewTaskServer
, lalu kita akan membicarakan tentang router dan penangan jalur.
NewTaskServer
Adalah konstruktor untuk server kami, bertipe
taskServer
. Server menyertakan
TaskStore
apa yang aman dalam hal akses data secara bersamaan .
type taskServer struct {
store *taskstore.TaskStore
}
func NewTaskServer() *taskServer {
store := taskstore.New()
return &taskServer{store: store}
}
Penanganan perutean dan jalur
Sekarang mari kembali ke perutean. Ini menggunakan multiplexer HTTP standar yang disertakan dalam paket
net/http
:
mux.HandleFunc("/task/", server.taskHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)
Multiplexer standar memiliki kemampuan yang agak sederhana. Ini adalah kekuatan dan kelemahannya. Keunggulannya adalah sangat mudah untuk mengatasinya, karena tidak ada yang sulit dalam pekerjaannya. Dan kelemahan dari multiplexer standar adalah terkadang penggunaannya membuat pemecahan masalah mencocokkan permintaan dengan jalur yang tersedia di sistem agak membosankan. Apa, menurut logika hal, alangkah baiknya terletak di satu tempat, Anda harus menempatkannya di tempat yang berbeda. Kami akan membicarakan lebih banyak tentang ini segera.
Karena multiplexer standar hanya mendukung pencocokan tepat permintaan ke prefiks jalur, kami secara praktis dipaksa untuk hanya mengandalkan jalur akar di tingkat atas dan mendelegasikan tugas untuk menemukan jalur yang tepat ke penangan jalur.
Mari kita periksa penangan jalur
taskHandler
:
func (ts *taskServer) taskHandler(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/task/" {
// "/task/", ID.
if req.Method == http.MethodPost {
ts.createTaskHandler(w, req)
} else if req.Method == http.MethodGet {
ts.getAllTasksHandler(w, req)
} else if req.Method == http.MethodDelete {
ts.deleteAllTasksHandler(w, req)
} else {
http.Error(w, fmt.Sprintf("expect method GET, DELETE or POST at /task/, got %v", req.Method), http.StatusMethodNotAllowed)
return
}
Kami memulai dengan memeriksa jalur yang sama persis dengan
/task/
(yang berarti tidak ada di bagian akhir
<taskid>
). Di sini kita perlu memahami metode HTTP mana yang digunakan dan memanggil metode server yang sesuai. Sebagian besar penangan jalur adalah pembungkus API yang cukup sederhana
TaskStore
. Mari kita lihat salah satu penangan ini:
func (ts *taskServer) getAllTasksHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get all tasks at %s\n", req.URL.Path)
allTasks := ts.store.GetAllTasks()
js, err := json.Marshal(allTasks)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
Ini menyelesaikan dua tugas utama:
- Menerima data dari model (
TaskStore
). - Menghasilkan respons HTTP untuk klien.
Kedua tugas ini cukup sederhana dan mudah, tetapi jika Anda memeriksa kode penangan jalur lain, Anda dapat melihat bahwa tugas kedua cenderung berulang - ini terdiri dari menyusun data JSON, menyiapkan header respons HTTP yang benar, dan dalam melakukan tindakan serupa lainnya. ... Kami akan mengangkat masalah ini lagi nanti.
Ayo kembali sekarang ke
taskHandler
. Sejauh ini, kami hanya melihat cara menangani permintaan yang memiliki jalur yang sama persis
/task/
. Bagaimana dengan jalannya
/task/<taskid>
? Di sinilah bagian kedua dari fungsi tersebut masuk:
} else {
// ID, "/task/<id>".
path := strings.Trim(req.URL.Path, "/")
pathParts := strings.Split(path, "/")
if len(pathParts) < 2 {
http.Error(w, "expect /task/<id> in task handler", http.StatusBadRequest)
return
}
id, err := strconv.Atoi(pathParts[1])
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if req.Method == http.MethodDelete {
ts.deleteTaskHandler(w, req, int(id))
} else if req.Method == http.MethodGet {
ts.getTaskHandler(w, req, int(id))
} else {
http.Error(w, fmt.Sprintf("expect method GET or DELETE at /task/<id>, got %v", req.Method), http.StatusMethodNotAllowed)
return
}
}
Ketika kueri tidak sama persis dengan jalur
/task/
, kami mengharapkan
ID
masalah numerik mengikuti garis miring . Kode di atas mem-parsing yang satu ini
ID
dan memanggil penangan yang sesuai (berdasarkan metode permintaan HTTP).
Sisa kode kurang lebih mirip dengan yang telah kita bahas, seharusnya mudah dipahami.
Peningkatan server
Sekarang kita memiliki versi dasar server yang berfungsi, saatnya untuk memikirkan tentang kemungkinan masalah yang mungkin timbul dengannya dan bagaimana memperbaikinya.
Salah satu konstruksi pemrograman yang kami gunakan yang jelas membutuhkan perbaikan, dan yang telah kami bicarakan, adalah kode berulang untuk menyiapkan data JSON saat membuat respons HTTP. Saya membuat versi server terpisah, stdlib-factorjson , yang menyelesaikan masalah ini. Saya telah memisahkan implementasi server ini ke dalam folder terpisah agar lebih mudah untuk membandingkannya dengan kode server asli dan menganalisis perubahannya. Inovasi utama kode ini diwakili oleh fungsi berikut:
// renderJSON 'v' JSON , , w.
func renderJSON(w http.ResponseWriter, v interface{}) {
js, err := json.Marshal(v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
Dengan menggunakan fungsi ini, kita dapat menulis ulang kode dari semua penangan jalur, memperpendeknya. Misalnya, berikut adalah tampilan kodenya sekarang
getAllTasksHandler
:
func (ts *taskServer) getAllTasksHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get all tasks at %s\n", req.URL.Path)
allTasks := ts.store.GetAllTasks()
renderJSON(w, allTasks)
}
Perbaikan yang lebih mendasar adalah membuat kode pemetaan permintaan-ke-jalur lebih bersih dan, jika memungkinkan, mengumpulkan kode ini di satu tempat. Meskipun pendekatan saat ini untuk mencocokkan permintaan dan jalur mempermudah proses debug, kode di baliknya sulit dipahami pada pandangan pertama karena tersebar di beberapa fungsi. Misalnya, kita mencoba untuk mencari tahu bagaimana permintaan
DELETE
yang diarahkan ke file
/task/<taskid>
. Untuk melakukannya, ikuti langkah-langkah berikut:
- - โ
main
,/task/
taskHandler
. - ,
taskHandler
,else
, ,/task/
.<taskid>
. - โ
if
, , , , ,DELETE
deleteTaskHandler
.
Anda dapat meletakkan semua kode ini di satu tempat. Akan jauh lebih mudah dan nyaman untuk bekerja dengannya. Inilah tujuan dari router HTTP pihak ketiga. Kami akan membicarakannya di bagian kedua dari seri artikel ini.
โ Ini adalah bagian pertama dari seri pengembangan server Go. Anda dapat melihat daftar artikel di awal materi asli ini.