Pengembangan server REST di Go. Bagian 1: perpustakaan standar

Ini adalah yang pertama dari serangkaian artikel tentang mengembangkan server REST di Go. Dalam artikel ini, saya berencana untuk menjelaskan implementasi server REST sederhana menggunakan beberapa pendekatan berbeda. Akibatnya, pendekatan ini dapat dibandingkan satu sama lain, akan mungkin untuk memahami keunggulan relatif mereka satu sama lain.



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:



  1. Menerima data dari model ( TaskStore



    ).
  2. 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:



  1. - โ€” main



    , /task/



    taskHandler



    .
  2. , taskHandler



    , else



    , , /task/



    . <taskid>



    .
  3. โ€” 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.








All Articles