GoでのRESTサーバーの開発。パート1:標準ライブラリ

これは、GoでのRESTサーバーの開発に関する一連の記事の最初の記事です。これらの記事では、いくつかの異なるアプローチを使用した単純なRESTサーバーの実装について説明する予定です。その結果、これらのアプローチを相互に比較することができ、相互の相対的な利点を理解することが可能になります。



Goを使い始めたばかりの開発者への最初の質問は、「問題Xを解決するためにどのフレームワークを使用すべきか」のように見えることがよくあります。他の多くの言語で書かれたウェブアプリケーションやサーバーを念頭に置いて尋ねられた場合、これは完全に正常な質問ですが、Goの場合、この質問に答えるときに考慮すべき多くの微妙な点があります。Goプロジェクトでフレームワークを使用することには賛否両論があります。このシリーズの記事に取り組んでいる間、私は私の目標をこの問題の客観的で用途の広い研究として見ています。



仕事



まず、読者が「RESTサーバー」の概念に精通していることを前提にしています。復習が必要な場合は、 この優れた資料をご覧ください(ただし、他にも同様の記事がたくさんあります)。これからは、「パス」、「HTTPヘッダー」、「応答コード」などの用語を使用すると、私が何を意味するのか理解できると思います。



この場合、サーバーは、タスク管理機能(Google Keep、Todoistなど)を実装するアプリケーションの単純なバックエンドシステムです。サーバーは、次のRESTAPIをクライアントに提供します。



POST   /task/              :       ID
GET    /task/<taskid>      :       ID
GET    /task/              :    
DELETE /task/<taskid>      :     ID
GET    /tag/<tagname>      :       
GET    /due/<yy>/<mm>/<dd> :    ,    

      
      





このAPIは、この例のために特別に作成されたものであることに注意してください。このシリーズの次回の記事では、API設計に対するより構造化され標準化されたアプローチについて説明します。



私たちのサーバーはGET、POST、DELETEリクエストをサポートしており、それらのいくつかは複数のパスを使用する機能を備えています。 APIの説明で山括弧(<...>



で示され ているのは、クライアントがリクエストの一部としてサーバーに提供するパラメーターを示しています。たとえば、リクエストは GET /task/42



、サーバーからID



42



。を使用してタスクを受信するように指示されます ID



一意のタスク識別子です。



データはJSON形式でエンコードされます。リクエスト実行時 POST /task/



クライアントは、作成するタスクのJSON表現をサーバーに送信します。また、同様に、これらのリクエストへの応答には、JSONデータが含まれています。特に、HTTP応答の本文に配置されます。



コード



次に、Goでサーバーコードを段階的に記述します。フルバージョンは ここにありますこれは、依存関係を使用しない自己完結型のGoモジュールです。プロジェクトディレクトリのクローンを作成するか、コンピュータにコピーした後、サーバーは何もインストールせずにすぐに次のコマンドを実行できます。



$ SERVERPORT=4112 go run .

      
      





SERVERPORT



接続を待機している間、ローカルサーバーでリッスンする任意のポートを使用できることに注意してください サーバーが起動した後、別のターミナルウィンドウを使用して、たとえばユーティリティを使用してサーバーを操作できます curl



他の同様のプログラムを使用して操作することもできます。サーバーにリクエストを送信するために使用されるコマンドの例は、この スクリプトにありますこのスクリプトを含むディレクトリには、自動サーバーテスト用のツールが含まれています。



モデル



サーバーのモデル(または「データ層」)について説明することから始めましょう。パッケージ taskstore



internal/taskstore



プロジェクトディレクトリ)にあります。これは、タスクを格納するデータベースを表す単純な抽象概念です。APIは次のとおりです。



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

      
      





これが型宣言 Task



です:



type Task struct {
  Id   int       `json:"id"`
  Text string    `json:"text"`
  Tags []string  `json:"tags"`
  Due  time.Time `json:"due"`
}

      
      





パッケージは taskstore



、単純な辞書を使用してこのAPIを実装し map[int]Task



、データをメモリに格納します。しかし、このAPIのデータベース駆動型実装を想像するのは難しいことではありません。実際のアプリケーションでは TaskStore



、さまざまなバックエンドで実装できるインターフェイスである可能性があります。しかし、簡単な例では、このAPIで十分です。練習したい場合は、TaskStore



MongoDBのようなものを使用して実装 してください。



サーバーの作業準備



main



私たちのサーバーの機能 は非常に単純です:



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))
}

      
      





チームのために少し時間を取って NewTaskServer



から、ルーターとパスハンドラーについて説明します。



NewTaskServer



タイプがサーバーのコンストラクターです taskServer



サーバーにはTaskStore



同時データアクセスに関して安全なものが含まれて ます



type taskServer struct {
  store *taskstore.TaskStore
}

func NewTaskServer() *taskServer {
  store := taskstore.New()
  return &taskServer{store: store}
}

      
      





ルーティングおよびパスハンドラー



それでは、ルーティングに戻りましょう。これは、パッケージに含まれている標準のHTTPマルチプレクサを使用します net/http







mux.HandleFunc("/task/", server.taskHandler)
mux.HandleFunc("/tag/", server.tagHandler)
mux.HandleFunc("/due/", server.dueHandler)

      
      





標準マルチプレクサの機能はかなり控えめです。これは彼の長所と短所の両方です。その長所は、作業に難しいことは何もないので、非常に扱いやすいことです。また、標準マルチプレクサの弱点は、その使用により、要求をシステムで使用可能なパスと照合する問題の解決がかなり面倒になる場合があることです。物事の論理によれば、1つの場所に配置するとよいのですが、別の場所に配置する必要があります。これについては、後ほど詳しく説明します。



標準マルチプレクサはパスプレフィックスへの要求の正確な一致のみをサポートするため、実際には、最上位のルートパスのみに依存し、正確なパスを見つけるタスクをパスハンドラに委任する必要があります。



パスハンドラーを調べてみましょう 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
    }

      
      





パスがと完全に一致するかどうかを確認することから始めます/task/



(つまり、最後にパス がないことを意味し <taskid>



ます)。ここでは、使用されているHTTPメソッドを理解し、対応するサーバーメソッドを呼び出す必要があります。ほとんどのパスハンドラーは、かなり単純なAPIラッパー TaskStore



です。これらのハンドラーの1つを見てみましょう。



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)
}

      
      





それは2つの主要なタスクを解決します:



  1. モデル(TaskStore



    からデータを受信します
  2. クライアントのHTTP応答を生成します。


これらのタスクはどちらも非常に単純で単純ですが、他のパスハンドラーのコードを調べると、2番目のタスクが繰り返される傾向があることがわかります。これは、JSONデータのマーシャリング、正しいHTTP応答ヘッダーの準備、および他の同様のアクションを実行する... この問題は後で再度提起します。



に戻りましょう taskHandler



これまでのところ、パスが完全に一致するリクエストを処理する方法のみを確認してきました /task/



パスは /task/<taskid>



どうですか?ここで、関数の2番目の部分が登場します。



} 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
  }
}

      
      





クエリがパスと正確/task/



一致しない場合、 数値のID



問題はスラッシュの後に続くと予想されます 上記のコードはこれを解析し ID



、適切なハンドラーを呼び出します(HTTPリクエストメソッドに基づく)。



残りのコードは、すでに説明したものとほぼ同じであり、理解しやすいはずです。



サーバーの改善



サーバーの基本的な動作バージョンができたので、サーバーで発生する可能性のある問題とその改善方法について考えてみましょう。



明らかに改善が必要で、すでに説明したプログラミング構造の1つは、HTTP応答を生成するときにJSONデータを準備するための反復コードです。この問題を解決する別のバージョンのサーバーstdlib-factorjsonを作成しました 。元のサーバーコードとの比較と変更の分析を容易にするために、このサーバー実装を別のフォルダーに分割しました。このコードの主な革新は、次の関数によって表されます。



// 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)
}

      
      





この関数を使用すると、すべてのパスハンドラーのコードを書き直して、短縮することができます。たとえば、コードは次のようになります 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)
}

      
      





より根本的な改善は、要求からパスへのマッピングコードをよりクリーンにし、可能であれば、このコードを1か所に収集することです。リクエストとパスを照合する現在のアプローチではデバッグが容易になりますが、その背後にあるコードは複数の関数に分散しているため、一見して理解するのは困難です。たとえば、に送信さDELETE



れるリクエストどのように送信されるかを把握しようとしているとし /task/<taskid>



ます。これを行うには、次の手順に従います。



  1. - — main



    , /task/



    taskHandler



    .
  2. , taskHandler



    , else



    , , /task/



    . <taskid>



    .
  3. if



    , , , , , DELETE



    deleteTaskHandler



    .


このすべてのコードを1か所にまとめることができます。それを操作する方がはるかに簡単で便利です。これはまさにサードパーティのHTTPルーターが目指しているものです。これらについては、この記事シリーズの第2部で説明します。



これは囲碁サーバー開発に関するシリーズの最初のパートです。この資料の原本の冒頭にある記事のリストを見ることができます








All Articles