Golang實現(xiàn)短網址/短鏈服務的開發(fā)筆記分享
項目地址:https://github.com/astak16/shortlink
錯誤處理
在處理業(yè)務邏輯時,如果出錯誤了,需要統(tǒng)一處理錯誤響應的格式,這樣可以方便前端處理錯誤信息
所以需要定義一個 Error 接口,它包含了 error 接口,以及一個 Status() 方法,用來返回錯誤的狀態(tài)碼
type Error interface { error Status() int }
這個接口用來判斷錯誤類型,在 go 中可以通過 e.(type) 判斷錯誤的類型
func respondWithError(w http.RespondWrite, err error) { switch e.(type) { case Error: respondWithJSON(w, e.Status(), e.Error()) default: respondWithJSON(w, http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } }
在 go 中 實現(xiàn) Error 接口,只需要實現(xiàn) Error() 和 Status() 方法即可
func () Error() string { return "" } func () Status() int { return 0 }
這樣定義的方法,只能返回固定的文本和狀態(tài)碼,如果想要返回動態(tài)內容,可以定義一個結構體
然后 Error 和 Status 方法接受 StatusError 類型
這樣只要滿足 StatusError 類型的結構體,就可以返回動態(tài)內容
所以上面的代碼可以修改為:
type StatusError struct { Code int Err error } func (se StatusError) Error() string { return se.Err.Error() } func (se StatusError) Status() int { return se.Code }
middlerware
RecoverHandler
中間件 RecoverHandler 作用是通過 defer 來捕獲 panic,然后返回 500 狀態(tài)碼
func RecoverHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { if r := recover(); r != nil { log.Println("Recover from panic %+v", r) http.Error(w, http.StatusText(500), 500) } }() next.ServeHTTP(w, r) } return http.HandlerFunc(fn) }
LoggingHandler
LoggingHandler 作用是記錄請求耗時
func (m Middleware) LoggingHandler(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { start := time.Now() next.ServeHTTP(w, r) end := time.Now() log.Printf("[%s] %q %v", r.Method, r.URL.Path, end.Sub(start)) } return http.HandlerFunc(fn) }
中間件使用
alice 是 go 中的一個中間件庫,可以通過 alice.New() 來添加中間件,具體使用如下:
m := alice.New(middleware.LoggingHandler, middleware.RecoverHandler) mux.Router.HandleFunc("/api/v1/user", m.ThenFunc(controller)).Methods("POST")
生成短鏈接
redis 連接
func NewRedisCli(addr string, passwd string, db int) *RedisCli { c := redis.NewClient(&redis.Options{ Addr: addr, Password: passwd, DB: db, }) if _, err := c.Ping().Result(); err != nil { panic(err) } return &RedisCli{Cli: c} }
生成唯一 ID
redis 可以基于一個鍵名生成一個唯一的自增 ID,這個鍵名可以是任意的,這個方法是 Incr
代碼如下:
err = r.Cli.Incr(URLIDKEY).Err() if err != nil { return "", err } id, err := r.Cli.Get(URLIDKEY).Int64() if err != nil { return "", err } fmt.Println(id) // 每次調用都會自增
存儲和解析短鏈接
一個 ID 對應一個 url,也就是說當外面?zhèn)魅?id 時需要返回對應的 url
func Shorten() { err := r.Cli.Set(fmt.Sprintf(ShortlinkKey, eid), url, time.Minute*time.Duration(exp)).Err() if err != nil { return "", err } } func UnShorten() { url, err := r.Cli.Get(fmt.Sprintf(ShortlinkKey, eid)).Result() }
redis 注意事項
redis 返回的 error 有兩種情況:
- redis.Nil 表示沒有找到對應的值
- 其他錯誤,表示 redis 服務出錯了
所以在使用 redis 時,需要判斷返回的錯誤類型
if err == redis.Nil { // 沒有找到對應的值 } else if err != nil { // redis 服務出錯了 } else { // 正確響應 }
測試
在測試用例中,如何發(fā)起一個請求,然后獲取響應的數據呢?
1.構造請求
var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`) req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json")
2.捕獲 http 響應
rw := httptest.NewRecorder()
3.模擬請求被處理
app.Router.ServeHTTP(rw, req)
4.解析響應
if rw.Code != http.ok { t.Fatalf("Excepted status created, got %d", rw.Code) } resp := struct { Shortlink string `json:"shortlink"` }{} if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil { t.Fatalf("should decode the response", err) }
最終完整代碼:
var jsonStr = []byte(`{"url":"https://www.baidu.com","expiration_in_minutes":60}`) req, err := http.NewRequest("POST", "/api/shorten", bytes.NewBuffer(jsonStr)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") rw := httptest.NewRecorder() app.Router.ServeHTTP(rw, req) if rw.Code != http.ok { t.Fatalf("Excepted status created, got %d", rw.Code) } resp := struct { Shortlink string `json:"shortlink"` }{} if err := json.NewDecoder(rw.Body).Decode(&resp); err != nil { t.Fatalf("should decode the response") }
代碼講解
log.SetFlags(log.LstdFlags | log.Lshortfile)
作用是設置日志輸出的標志
它們都是標志常量,用豎線 | 連接,這是位操作符,將他們合并為一個整數值,作為 log.SetFlags() 的參數
- log.LstdFlags 是標準時間格式:2022-01-23 01:23:23
- log.Lshortfile 是文件名和行號:main.go:23
當我們使用 log.Println 輸出日志時,會自動帶上時間、文件名、行號信息
recover 函數使用
recover 函數類似于其他語言的 try...catch,用來捕獲 panic,做一些處理
使用方法:
func MyFunc() { defer func() { if r := recover(); r != nil { // 處理 panic 情況 } } }
需要注意的是:
- recover 函數只能在 defer 中使用,如果在 defer 之外使用,會直接返回 nil
- recover 函數只有在 panic 之后調用才會生效,如果在 panic 之前調用,也會直接返回 nil
- recover 函數只能捕獲當前 goroutine 的 panic,不能捕獲其他 goroutine 的 panic
next.ServerHttp(w, r)
next.ServeHTTP(w, r),用于將 http 請求傳遞給下一個 handler
HandleFunc 和 Handle 區(qū)別
HandleFunc 接受一個普通類型的函數:
func myHandle(w http.ResponseWriter, r *http.Request) {} http.HandleFunc("xxxx", myHandle)
Handle 接收一個實現(xiàn) Handler 接口的函數:
func myHandler(w http.ResponseWriter, r *http.Request) {} http.Handle("xxxx", http.HandlerFunc(myHandler))
他們的區(qū)別是:使用 Handle 需要自己進行包裝,使用 HandleFunc 不需要
defer res.Body.Close()
為什么沒有 res.Header.Close() 方法?
因為 header 不是資源,而 body 是資源,在 go 中,一般操作資源后,要及時關閉資源,所以 go 為 body 提供了 Close() 方法
res.Body 是 io.ReadCloser 類型的接口,表示可以讀取響應數據并關閉響應體的對象
w.Write()
代碼在執(zhí)行了 w.Writer(res) 后,還會繼續(xù)往下執(zhí)行,除非有顯示的 reture 和 panic 終止函數執(zhí)行
func controller(w http.ResponseWriter, r *http.Request) { if res, err := xxx; err != nil { respondWithJSON(w, http.StatusOK, err) } // 這里如果有代碼,會繼續(xù)執(zhí)行 } func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { res, _ json.Marshal(payload) w.Header().Set("Content-Type", "application/json") w.WriteHeader(code) w.Write(res) }
需要注意的是,盡管執(zhí)行了 w.Writer() 后,還會繼續(xù)往下執(zhí)行,但不會再對響應進行修改或寫入任何內容了,因為 w.Write() 已經將響應寫入到 http.ResponseWriter 中了
獲取請求參數
路由 /api/info?shortlink=2
a.Router.Handle("/api/info", m.ThenFunc(a.getShortlinkInfo)).Methods("GET") func getShortlinkInfo(w http.ResponseWriter, r *http.Request) { vals := r.URL.Query() s := vals.Get("shortlink") fmt.Println(s) // 2 }
路由 /2
a.Router.Handle("/{shortlink:[a-zA-Z0-9]{1,11}}", m.ThenFunc(a.redirect)).Methods("GET") func redirect(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) shortlink := vars["shortlink"] fmt.Println(shortlink) // 2 }
獲取請求體
json.NewDecoder(r.Body) 作用是將 http 請求的 body 內容解析為 json 格式
r.body 是一個 io.Reader 類型,它代表請求的原始數據
如果關聯(lián)成功可以用 Decode() 方法來解析 json 數據
type User struct { Name string `json:"name"` Age int `json:"age"` } func controller(w http.ResponseWriter, r *http.Request){ var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { fmt.Println(err) } fmt.Println(user) }
new
用于創(chuàng)建一個新的零值對象,并返回該對象的指針
它接受一個類型作為參數,并返回一個指向該類型的指針
適用于任何可分配的類型,如基本類型、結構體、數組、切片、映射和接口等
// 創(chuàng)建一個新的 int 類型的零值對象,并返回指向它的指針 ptr := new(int) // 0
需要注意的是:new 只分配了內存,并初始化為零值,并不會對對象進行任何進一步的初始化。如果需要對對象進行自定義的初始化操作,可以使用結構體字面量或構造函數等方式
到此這篇關于Golang實現(xiàn)短網址/短鏈服務的開發(fā)筆記分享的文章就介紹到這了,更多相關Golang短網址內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解Go并發(fā)編程時如何避免發(fā)生競態(tài)條件和數據競爭
大家都知道,Go是一種支持并發(fā)編程的編程語言,但并發(fā)編程也是比較復雜和容易出錯的。比如本篇分享的問題:競態(tài)條件和數據競爭的問題2023-04-04