Golang Web入門(2):RESTfulルーティングを実装する方法

まとめ

では前回の記事、私たちは中にHTTPサーバGolangを実現する方法をおしゃべり。しかし最終的にはDefaultServeMux、ルーティング分布関数として使用できますが、その機能も不完全であることがわかります

DefaultServeMuxルート配布を行うことでRESTfulスタイルAPI を実装することは不可能であり、リクエストで必要なメソッドを定義する方法も、APIパスにqueryパラメーターを追加することもできません次に、ルーティングルックアップをより効率的にしたいと考えています。

したがって、この記事では、httprouterこのパッケージを分析し、前述の機能をソースレベルから実装する方法を研究します。また、このパッケージ最も重要な接頭辞ツリーについては、この記事はグラフィックスとテキストの組み合わせで説明されます。

1回の使用

また、使用方法から始めて、上から下まで学習しhttprouterます。公式ドキュメントの小さな例を見てみましょう:

package main

import (
    "fmt"
    "net/http"
    "log"

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}
复制代码

実際、ここnet/httpでのアプローチは、Golang独自のパッケージを使用するアプローチに似ていることがわかります。すべてが対応するURIと関数を最初に登録します。つまり、ルートをプロセッサと照合します。

登録時に、使用router.XXXたとえば、対応するメソッドを登録する方法をGETPOSTそして上のように。

登録後、を使用してhttp.ListenAndServe監視開始します。

理由については、後の章で詳しく紹介しますが、ここでは最初に練習を理解するだけで済みます。

2作成

コードの1行目を見てみましょう。1つを定義して宣言しましたRouterRouterこれの構造を見て、この記事に関係のない他の属性は省略します。

type Router struct {
	//这是前缀树,记录了相应的路由
	trees map[string]*node
	
	//记录了参数的最大数目
	maxParams  uint16

}
复制代码

このRouter構造を作成した後router.XXXメソッドを使用してルートを登録します。ルートの登録方法を引き続き確認します。

func (r *Router) GET(path string, handle Handle) {
	r.Handle(http.MethodGet, path, handle)
}

func (r *Router) POST(path string, handle Handle) {
	r.Handle(http.MethodPost, path, handle)
}

...
复制代码

ここにはメソッドの長いリストがあり、それらはすべて同じです。

r.Handle(http.MethodPost, path, handle)
复制代码

この方法。もう一度見てみましょう:

func (r *Router) Handle(method, path string, handle Handle) {
	...
	if r.trees == nil {
		r.trees = make(map[string]*node)
	}

	root := r.trees[method]
	if root == nil {
		root = new(node)
		r.trees[method] = root

		r.globalAllowed = r.allowed("*", "")
	}

	root.addRoute(path, handle)
	...
}
复制代码

この方法では、多くの詳細も省略されています。この記事に関連するものにのみ焦点を当てます。このメソッドでは、tree初期化されていない場合接頭辞ツリーが最初に初期化されていることがわかります

次に、このツリーがmap構造であることに気付きましたつまり、メソッドはツリーに対応します。次に、このツリーに対応して、addRouteメソッドURI呼び出しHandle対応するを保存します。

3接頭辞ツリー

3.1定義

単語検索ツリーとも呼ばれるTrieツリーは、ツリー構造であり、ハッシュツリーのバリアントです。典型的なアプリケーションは、多数の文字列(文字列に限定されない)をカウント、ソート、および保存することです。そのため、テキストワードの頻度統計のために検索エンジンシステムでよく使用されます。その利点は次のとおりです。文字列の共通のプレフィックスを使用してクエリ時間を短縮し、不要な文字列比較を最小限に抑え、クエリ効率はハッシュツリーよりも高いです。

簡単に言えば、あなたが探しているもの、この木の特定の道をたどっている限り、あなたはそれを見つけることができます。

例えば、検索エンジンは、あなたが入力したカイを

彼にはこれらの関連付けがあり、これは接頭辞ツリーとしても理解できます。

別の例:

このGETメソッドプレフィックスツリーには、次のルートが含まれています。

  • /うわ〜すごい
  • /テスト
  • /こんにちは世界
  • / hello / china
  • / hello / chinese

この時点で、このツリーを構築する過程で、任意の2つのノードが同じプレフィックスを持っている限り、同じパーツが1つのノードにマージされることを理解する必要があります

3.2グラフィカルな構造

上記のaddRoute方法は、この接頭辞ツリーを挿入する方法です。数が空であると仮定して、ここでこのツリーの構造をグラフィカルに説明するつもりです。

挿入する必要がある3つのルートは次のとおりです。

  • /こんにちは世界
  • / hello / china
  • / hello / chinese

(1)挿入/hello/world

この時点ではツリーは空なので、直接挿入できます。

(2)挿入/hello/china

このケースでは、我々が見つかりました。/hello/world/hello/china同じプレフィックスを持ちます/hello/

次に、元の/hello/worldノードを最初に分割し、次に挿入するノードを子ノード/hello/chinaとして切り離し/hello/worldます。

(3)挿入/hello/chinese

この時点で、我々は挿入する必要があり/hello/chineseますが、発見、/hello/chineseおよびノードが/hello/共通のプレフィックスを持っている/hello/私たちが見に行ったので、/hello/このノードの子ノードを。

ノードにはと呼ばれる属性があることに注意してくださいindicesこれは、このノードの子ノードの最初の文字を記録するため、簡単に見つけることができます。たとえば、この/hello/ノードには彼のindices値がありますwc挿入するノードは/hello/chinese、共通のプレフィックスを削除した後chineseの最初の文字cので、chinaこのノードに入ります。

このとき、最初に挿入/hello/chinaしたときの状態に戻ったと思いますか?当時、パブリックプレフィックスはでしたが/hello/、現在、パブリックプレフィックスはchinです。

したがって、このノードの子chinとなるノードとして切り出しaます。また、ese子ノードとも見なされます。

3.3構築アルゴリズムのまとめ

この時点で、建設は終わりました。アルゴリズムを要約しましょう。

特定の注釈付きコードはこの記事の最後に記載されています。詳しく知りたい場合は、自分で確認できます。まずここでプロセスを理解します。

(1)ツリーが空の場合は直接挿入する
(2)そうでない場合は、現在のノードに挿入するノードURIと共通のプレフィックスがあるかどうかを確認する(3)共通のプレフィックスがない場合は直接挿入する(4)共通のプレフィックスがある場合は判断する現在のノードを分割する必要がありますか
(5)分割する必要がある場合は、共通部分を親ノードとして使用し、残りを子ノードとして使用します
(6)分割する必要がない場合は、同じプレフィックスを持つ子ノードを探します
(7)プレフィックスがある場合同じである場合は、(4)にスキップします。(
8)同じプレフィックスがない場合は
、最後のノードに(9)を直接挿入し、このルートをHandle

しかし、ここで、いくつかの学生は尋ねなければなりません:なぜここにパラメータなしのルートがあるのですか?

実際、上記のプロセスを理解していれば、パラメーターは同じです。ロジックはこれです:各挿入前に、現在の経路(すなわち、そこには走査されていないまたはパラメータを指定せずにノードを挿入するためにスキャンされる/又は*)。パラメータがある場合wildChild、現在のノードの属性をに設定してからtrue、パラメータ部分を新しい子ノードに設定します

4モニター

ルーティング登録について話した後、ルーティング監視について話しましょう。

では前の記事内容、我々はこれを言及しました。

type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}
复制代码

そのときに、Handleメソッドを渡さない場合、GolangはデフォルトのDefaultServeMuxメソッドを使用してリクエストを処理することを述べましたこれでが渡されたrouterのでrouter、リクエストの処理に使用されます。

したがって、メソッドrouterも実現されServeHTTPます。見てみましょう(一部の手順も省略されています)。

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
	path := req.URL.Path

	if root := r.trees[req.Method]; root != nil {
		if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
			if ps != nil {
				handle(w, req, *ps)
				r.putParams(ps)
			} else {
				handle(w, req, nil)
			}
			return
		} 
	}
    ...
    // Handle 404
	if r.NotFound != nil {
		r.NotFound.ServeHTTP(w, req)
	} else {
		http.NotFound(w, req)
	}
}
复制代码

ここではリクエストメソッドに対応するプレフィックスツリーを選択しメソッドを呼び出しますgetValue

このメソッドを簡単に説明します。このメソッドpathでは、このルートに対応するHandleメソッドが見つかるまで、現在のパスとノードを継続的に照合します

この期間中、ルートがRESTfulであり、ルートにパラメーターが含まれている場合、ルートはに保存されParamます。ここでのParam構造次のとおりです。

type Param struct {
	Key   string
	Value string
}
复制代码

対応するルートが見つからない場合は、次の404メソッドが呼び出されます。

5処理

この時点では、以前とほとんど同じです。

対応するルートを取得した後Handle、この関数を呼び出します。

前のnet/httpパッケージHandlerの唯一の違いは、ここHandleでは、APIから取得されたパラメーターがカプセル化されることです。

type Handle func(http.ResponseWriter, *http.Request, Params)
复制代码

最後に6

ご覧いただきありがとうございます〜

この時点で、httprouterの導入が完了しました。最も重要なのは接頭辞ツリーの構築です。上記では、グラフィックとテキストの組み合わせを使用して、接頭辞ツリーの構築プロセスをシミュレートしましたが、接頭辞ツリーが何であるかを理解していただければ幸いです。もちろん、まだ質問がある場合は、WeChatでメッセージを残したり、私と通信したりできます〜

もちろん、あなたがこれに満足していない場合、あなたが見ることができるの背面に取り付けたカタログ、プレフィックスツリーがあり、完全なコードのコメントは

もちろん、作者はまだ始まったばかりです。したがって、多くの省略があるかもしれません。読解の過程で不適切な説明がある場合、または理解に逸脱がある場合は、メッセージを残して訂正してください。

ありがとうございます〜

PS:他に質問がある場合は、公開アカウントで著者を見つけることもできます。さらに、すべての記事は最初にパブリックアカウントで更新されます。

7ソースコードの読み取り

7.1ツリー構造

type node struct {
	
	path      string    //当前结点的URI
	indices   string    //子结点的首字母
	wildChild bool      //子节点是否为参数结点
	nType     nodeType  //结点类型
	priority  uint32    //权重
	children  []*node   //子节点
	handle    Handle    //处理器
}
复制代码

7.2 addRoute

func (n *node) addRoute(path string, handle Handle) {

	fullPath := path
	n.priority++

	// 如果这是个空树,那么直接插入
	if len(n.path) == 0 && len(n.indices) == 0 {

		//这个方法其实是在n这个结点插入path,但是会处理参数
		//详细实现在后文会给出
		n.insertChild(path, fullPath, handle)
		n.nType = root
		return
	}

	//设置一个flag
walk:
	for {
		// 找到当前结点path和要插入的path中最长的前缀
		// i为第一位不相同的下标
		i := longestCommonPrefix(path, n.path)

		// 此时相同的部分比这个结点记录的path短
		// 也就是说需要把当前的结点分裂开
		if i < len(n.path) {
			child := node{

				// 把不相同的部分设置为一个切片,作为子节点
				path:      n.path[i:],
				wildChild: n.wildChild,
				nType:     static,
				indices:   n.indices,
				children:  n.children,
				handle:    n.handle,
				priority:  n.priority - 1,
			}

			// 将新的结点作为这个结点的子节点
			n.children = []*node{&child}
			// 把这个结点的首字母加入indices中
			// 目的是查找更快
			n.indices = string([]byte{n.path[i]})
			n.path = path[:i]
			n.handle = nil
			n.wildChild = false
		}

		// 此时相同的部分只占了新URI的一部分
		// 所以把path后面不相同的部分要设置成一个新的结点
		if i < len(path) {
			path = path[i:]

			// 此时如果n的子节点是带参数的
			if n.wildChild {
				n = n.children[0]
				n.priority++

				// 判断是否会不合法
				if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
					n.nType != catchAll &&
					(len(n.path) >= len(path) || path[len(n.path)] == '/') {
					continue walk
				} else {
					pathSeg := path
					if n.nType != catchAll {
						pathSeg = strings.SplitN(pathSeg, "/", 2)[0]
					}
					prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
					panic("'" + pathSeg +
						"' in new path '" + fullPath +
						"' conflicts with existing wildcard '" + n.path +
						"' in existing prefix '" + prefix +
						"'")
				}
			}

			// 把截取的path的第一位记录下来
			idxc := path[0]

			// 如果此时n的子节点是带参数的
			if n.nType == param && idxc == '/' && len(n.children) == 1 {
				n = n.children[0]
				n.priority++
				continue walk
			}

			// 这一步是检查拆分出的path,是否应该被合并入子节点中
			// 具体例子可看上文中的图解
			// 如果是这样的话,把这个子节点设置为n,然后开始一轮新的循环
			for i, c := range []byte(n.indices) {
				if c == idxc {
					// 这一部分是为了把权重更高的首字符调整到前面
					i = n.incrementChildPrio(i)
					n = n.children[i]
					continue walk
				}
			}

			// 如果这个结点不用被合并
			if idxc != ':' && idxc != '*' {
				// 把这个结点的首字母也加入n的indices中
				n.indices += string([]byte{idxc})
				child := &node{}
				n.children = append(n.children, child)
				n.incrementChildPrio(len(n.indices) - 1)
				// 新建一个结点
				n = child
			}
			// 对这个结点进行插入操作
			n.insertChild(path, fullPath, handle)
			return
		}

		// 直接插入到当前的结点
		if n.handle != nil {
			panic("a handle is already registered for path '" + fullPath + "'")
		}
		n.handle = handle
		return
	}
}
复制代码

7.3 insertChild

func (n *node) insertChild(path, fullPath string, handle Handle) {
	for {
		// 这个方法是用来找这个path是否含有参数的
		wildcard, i, valid := findWildcard(path)
		// 如果不含参数,直接跳出循环,看最后两行
		if i < 0 {
			break
		}

		// 条件校验
		if !valid {
			panic("only one wildcard per path segment is allowed, has: '" +
				wildcard + "' in path '" + fullPath + "'")
		}

		// 同样判断是否合法
		if len(wildcard) < 2 {
			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
		}

		if len(n.children) > 0 {
			panic("wildcard segment '" + wildcard +
				"' conflicts with existing children in path '" + fullPath + "'")
		}

		// 如果参数的第一位是`:`,则说明这是一个参数类型
		if wildcard[0] == ':' {
			if i > 0 {
				// 把当前的path设置为参数之前的那部分
				n.path = path[:i]
				// 准备把参数后面的部分作为一个新的结点
				path = path[i:]
			}

			//然后把参数部分作为新的结点
			n.wildChild = true
			child := &node{
				nType: param,
				path:  wildcard,
			}
			n.children = []*node{child}
			n = child
			n.priority++

			// 这里的意思是,path在参数后面还没有结束
			if len(wildcard) < len(path) {
				// 把参数后面那部分再分出一个结点,continue继续处理
				path = path[len(wildcard):]
				child := &node{
					priority: 1,
				}
				n.children = []*node{child}
				n = child
				continue
			}

			// 把处理器设置进去
			n.handle = handle
			return

		} else { // 另外一种情况
			if i+len(wildcard) != len(path) {
				panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
			}

			if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
				panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
			}

			// 判断在这之前有没有一个/
			i--
			if path[i] != '/' {
				panic("no / before catch-all in path '" + fullPath + "'")
			}

			n.path = path[:i]

			// 设置一个catchAll类型的子节点
			child := &node{
				wildChild: true,
				nType:     catchAll,
			}
			n.children = []*node{child}
			n.indices = string('/')
			n = child
			n.priority++

			// 把后面的参数部分设置为新节点
			child = &node{
				path:     path[i:],
				nType:    catchAll,
				handle:   handle,
				priority: 1,
			}
			n.children = []*node{child}

			return
		}
	}

	// 对应最开头的部分,如果这个path里面没有参数,直接设置
	n.path = path
	n.handle = handle
}
复制代码

最も重要な方法はここにあります。ここにいるあなたに拍手を送りましょう!

この部分は理解するのが難しく、数回読む必要があるかもしれません。

それでもわからないところがある場合は、メッセージを残して交換するか、直接公開番号に来て見つけてください〜

おすすめ

転載: juejin.im/post/5e9bc84551882573c85aea78
おすすめ