iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 2
0

router

gin的router不是很方便,我們定義的接口只能寫成以下形式,之後再靠自己來處理參數

  • /:owner
  • /:owner/*project

解釋:

  • 「:」與「*」都能以變數的方法來存該位置的url字串
  • 「:」代表該位置必須要有一個以上的字符才會match
  • 「*」會match到所有字符,包含「\」

code

正式來寫gin吧!
在app中創建router package,用來解析路由,在裡面創建main.go,接著創建serve package,用來放最後處理的function

main router

在router中創建main.go寫入

package router

import (
	"app/setting"
	"app/serve"
	"github.com/gin-gonic/gin"
)

func MainRouter() *gin.Engine {
	r := gin.New()

	gin.SetMode(setting.Servers["main"].RunMode)

	/*home page*/
	r.GET("/", serve.GetRoot)

	/*owner*/
	r.GET("/:owner", serve.GetOwner)

	/*works*/
	r.GET("/:owner/*project", serve.GetProject)

	/*auth*/
	owner := r.Group("/:owner")
	{
		work := owner.Group("/*work")
		{
			work.POST("", serve.CreateBlog)
			work.PUT("", serve.UpdateBlog)
			work.DELETE("", serve.DelBlog)
		}
	}
	return r
}

基本的router,post, put, delete等操作需要登錄驗證,之後需要寫middleware來處理。

serve

在serve中創建main.go,寫個雛型

package serve

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"strings"
)

/*home page*/

// get root
func GetRoot(c *gin.Context) {

}

/*owner*/

func GetOwner(c *gin.Context) {

}

/*project*/

func GetBlog(c *gin.Context) {
	projs, blog := splitProject(c.Param("project"))
	c.JSON(http.StatusOK, gin.H{
		"owner" : c.Param("owner"),
		"project" : projs,
		"blog" : blog,
	})
}

func PostProject(c *gin.Context){

}

func PutProject(c *gin.Context){

}

func DelProject(c *gin.Context){

}

// split url to slice of projects and last project or blog
func splitWork(url string) ([]string, string) {
	works := strings.Split(url, "/")
	return works[:len(works)-1], works[len(works)-1]
}

解釋:

  • c.JSON回傳json給client
  • gin.H是map["string"]interface{},很適合用來作為JSON對象
  • splitWork將/foo/bar/abc分割成project ["foo", "bar"]與blog "abc"方便之後操作

寫個test
創建main_test.go,寫入

package serve

import (
	"testing"
)

func TestServe_SplitWork(t *testing.T) {
	type expect struct {
		project []string
		blog    string
	}

	// build test table
	var splitTest = []struct {
		url     string // input
		expect_ expect // expected result
	}{
		{"/foo", expect{[]string{""}, "foo"}},
		{"/foo/bar", expect{[]string{"", "foo"}, "bar"}},
		{"/foo/bar/blog", expect{[]string{"", "foo", "bar"}, "blog"}},
	}

	for _, value := range splitTest {
		proj, blog := splitWork(value.url)
		for i, v := range proj {
			if v != value.expect_.project[i] {
				t.Errorf("splitWork(%s).project[%d] = %s; expected %s\n", value.url, i, v, value.expect_.project[i])
			}
		}
		// test blog
		if blog != value.expect_.blog {
			t.Errorf("splitWork(%s).blog = %s; expected %s\n", value.url, blog, value.expect_.blog)
		}
	}
}

解釋:

  • splitTest是一個TestTable,要修改test案例只要改一行就好
  • range會返回slice的index與copy value

跑一下結果

go test ./serve
ok      app/serve       0.005s

我們的路由已經註冊完了,沒辦法定義其他服務(使用者登入,載入js, css檔等等...),解決辦法有兩種

  1. 在HandleFunc裡面使用if判斷
  2. subdomain

這邊我選擇使用subdomain,但是gin本身也不支援,所以必須從ServeHTTP下手。

subdomain

在router中創建host_switch.go,寫代碼之前,以下是我參考的資料

  • subdomain的方法可以看這篇
  • ServeHTTP源碼可以看這篇

寫入

package router

import (
	"net/http"
	"net/url"
)

// We need an object that implements the http.Handler interface.
// Therefore we need a type for which we implement the ServeHTTP method.
// We just use a map here, in which we map host names (with port) to http.Handlers
type HostSwitch map[string]http.Handler

// Implement the ServeHTTP method on our new type
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// Check if a http.Handler is registered for the given host.
	// If yes, use it to handle the request.
	if handler := hs[r.Host]; handler != nil {
		handler.ServeHTTP(w, r)
	} else {
		// Handle host names for which no handler is registered
		http.Error(w, "Forbidden", 403) // Or Redirect?
	}
}

main

在main.go寫入

package main

import (
	"app/router"
	"app/setting"

	"fmt"
	"log"
	"net/http"
)

var hs router.HostSwitch

func main() {
	mainRouter := router.MainRouter()

	// Make a new HostSwitch and insert the router (our http handler)
	hs = make(router.HostSwitch)
	hs[fmt.Sprintf("%s:%d", setting.Servers["main"].Host, setting.Servers["main"].Port)] = mainRouter

	s := &http.Server{
		Addr:           fmt.Sprintf(":%d", setting.Servers["main"].Port),
		Handler:        hs,
		ReadTimeout:    setting.Servers["main"].ReadTimeout,
		WriteTimeout:   setting.Servers["main"].WriteTimeout,
	}

	log.Fatal(s.ListenAndServe())
}

解釋:

  • http.ListenAndServe()使用默認的http.Server並沒有設置time out,所以這邊自己new一個server

總結

現在應該能跑起來了
輸入

go run main.go

目前的工作環境

.
├── app
│   ├── config
│   │   └── app
│   │       └── app.yaml
│   ├── go.mod
│   ├── go.sum
│   ├── main.go
│   ├── router
│   │   ├── host_switch.go
│   │   └── main.go
│   ├── serve
│   │   ├── main.go
│   │   └── main_test.go
│   └── setting
│       └── setting.go
└── database

題外話,想要在local host玩玩subdomain可以看這篇


上一篇
Day1 環境建構
下一篇
Day3 URL處理
系列文
從coding到上線-打造自己的blog系統30

1 則留言

1
screenleon
iT邦新手 4 級 ‧ 2020-09-03 11:36:51

你有些go打錯成gin了

linyanbin iT邦新手 5 級 ‧ 2020-09-03 15:20:34 檢舉

抱歉,我搜了一下沒發現,應該都是要說gin沒錯,可能是我的表達能力不好

喔喔 不好意思 我誤會了

我要留言

立即登入留言