Golang實(shí)現(xiàn)短網(wǎng)址/短鏈服務(wù)的開(kāi)發(fā)筆記分享
項(xiàng)目地址:https://github.com/astak16/shortlink
錯(cuò)誤處理
在處理業(yè)務(wù)邏輯時(shí),如果出錯(cuò)誤了,需要統(tǒng)一處理錯(cuò)誤響應(yīng)的格式,這樣可以方便前端處理錯(cuò)誤信息
所以需要定義一個(gè) Error 接口,它包含了 error 接口,以及一個(gè) Status() 方法,用來(lái)返回錯(cuò)誤的狀態(tài)碼
type Error interface {
error
Status() int
}這個(gè)接口用來(lái)判斷錯(cuò)誤類(lèi)型,在 go 中可以通過(guò) e.(type) 判斷錯(cuò)誤的類(lèi)型
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 中 實(shí)現(xiàn) Error 接口,只需要實(shí)現(xiàn) Error() 和 Status() 方法即可
func () Error() string {
return ""
}
func () Status() int {
return 0
}這樣定義的方法,只能返回固定的文本和狀態(tài)碼,如果想要返回動(dòng)態(tài)內(nèi)容,可以定義一個(gè)結(jié)構(gòu)體
然后 Error 和 Status 方法接受 StatusError 類(lèi)型
這樣只要滿(mǎn)足 StatusError 類(lèi)型的結(jié)構(gòu)體,就可以返回動(dòng)態(tài)內(nè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 作用是通過(guò) defer 來(lái)捕獲 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 作用是記錄請(qǐng)求耗時(shí)
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 中的一個(gè)中間件庫(kù),可以通過(guò) alice.New() 來(lái)添加中間件,具體使用如下:
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 可以基于一個(gè)鍵名生成一個(gè)唯一的自增 ID,這個(gè)鍵名可以是任意的,這個(gè)方法是 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) // 每次調(diào)用都會(huì)自增
存儲(chǔ)和解析短鏈接
一個(gè) ID 對(duì)應(yīng)一個(gè) url,也就是說(shuō)當(dāng)外面?zhèn)魅?id 時(shí)需要返回對(duì)應(yīng)的 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 注意事項(xiàng)
redis 返回的 error 有兩種情況:
- redis.Nil 表示沒(méi)有找到對(duì)應(yīng)的值
- 其他錯(cuò)誤,表示 redis 服務(wù)出錯(cuò)了
所以在使用 redis 時(shí),需要判斷返回的錯(cuò)誤類(lèi)型
if err == redis.Nil {
// 沒(méi)有找到對(duì)應(yīng)的值
} else if err != nil {
// redis 服務(wù)出錯(cuò)了
} else {
// 正確響應(yīng)
}測(cè)試
在測(cè)試用例中,如何發(fā)起一個(gè)請(qǐng)求,然后獲取響應(yīng)的數(shù)據(jù)呢?
1.構(gòu)造請(qǐng)求
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 響應(yīng)
rw := httptest.NewRecorder()
3.模擬請(qǐng)求被處理
app.Router.ServeHTTP(rw, req)
4.解析響應(yīng)
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)
作用是設(shè)置日志輸出的標(biāo)志
它們都是標(biāo)志常量,用豎線 | 連接,這是位操作符,將他們合并為一個(gè)整數(shù)值,作為 log.SetFlags() 的參數(shù)
- log.LstdFlags 是標(biāo)準(zhǔn)時(shí)間格式:2022-01-23 01:23:23
- log.Lshortfile 是文件名和行號(hào):main.go:23
當(dāng)我們使用 log.Println 輸出日志時(shí),會(huì)自動(dòng)帶上時(shí)間、文件名、行號(hào)信息
recover 函數(shù)使用
recover 函數(shù)類(lèi)似于其他語(yǔ)言的 try...catch,用來(lái)捕獲 panic,做一些處理
使用方法:
func MyFunc() {
defer func() {
if r := recover(); r != nil {
// 處理 panic 情況
}
}
}需要注意的是:
- recover 函數(shù)只能在 defer 中使用,如果在 defer 之外使用,會(huì)直接返回 nil
- recover 函數(shù)只有在 panic 之后調(diào)用才會(huì)生效,如果在 panic 之前調(diào)用,也會(huì)直接返回 nil
- recover 函數(shù)只能捕獲當(dāng)前 goroutine 的 panic,不能捕獲其他 goroutine 的 panic
next.ServerHttp(w, r)
next.ServeHTTP(w, r),用于將 http 請(qǐng)求傳遞給下一個(gè) handler
HandleFunc 和 Handle 區(qū)別
HandleFunc 接受一個(gè)普通類(lèi)型的函數(shù):
func myHandle(w http.ResponseWriter, r *http.Request) {}
http.HandleFunc("xxxx", myHandle)Handle 接收一個(gè)實(shí)現(xiàn) Handler 接口的函數(shù):
func myHandler(w http.ResponseWriter, r *http.Request) {}
http.Handle("xxxx", http.HandlerFunc(myHandler))他們的區(qū)別是:使用 Handle 需要自己進(jìn)行包裝,使用 HandleFunc 不需要
defer res.Body.Close()
為什么沒(méi)有 res.Header.Close() 方法?
因?yàn)?header 不是資源,而 body 是資源,在 go 中,一般操作資源后,要及時(shí)關(guān)閉資源,所以 go 為 body 提供了 Close() 方法
res.Body 是 io.ReadCloser 類(lèi)型的接口,表示可以讀取響應(yīng)數(shù)據(jù)并關(guān)閉響應(yīng)體的對(duì)象
w.Write()
代碼在執(zhí)行了 w.Writer(res) 后,還會(huì)繼續(xù)往下執(zhí)行,除非有顯示的 reture 和 panic 終止函數(shù)執(zhí)行
func controller(w http.ResponseWriter, r *http.Request) {
if res, err := xxx; err != nil {
respondWithJSON(w, http.StatusOK, err)
}
// 這里如果有代碼,會(huì)繼續(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() 后,還會(huì)繼續(xù)往下執(zhí)行,但不會(huì)再對(duì)響應(yīng)進(jìn)行修改或?qū)懭肴魏蝺?nèi)容了,因?yàn)?w.Write() 已經(jīng)將響應(yīng)寫(xiě)入到 http.ResponseWriter 中了
獲取請(qǐng)求參數(shù)
路由 /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
}獲取請(qǐng)求體
json.NewDecoder(r.Body) 作用是將 http 請(qǐng)求的 body 內(nèi)容解析為 json 格式
r.body 是一個(gè) io.Reader 類(lèi)型,它代表請(qǐng)求的原始數(shù)據(jù)
如果關(guān)聯(lián)成功可以用 Decode() 方法來(lái)解析 json 數(shù)據(jù)
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)建一個(gè)新的零值對(duì)象,并返回該對(duì)象的指針
它接受一個(gè)類(lèi)型作為參數(shù),并返回一個(gè)指向該類(lèi)型的指針
適用于任何可分配的類(lèi)型,如基本類(lèi)型、結(jié)構(gòu)體、數(shù)組、切片、映射和接口等
// 創(chuàng)建一個(gè)新的 int 類(lèi)型的零值對(duì)象,并返回指向它的指針 ptr := new(int) // 0
需要注意的是:new 只分配了內(nèi)存,并初始化為零值,并不會(huì)對(duì)對(duì)象進(jìn)行任何進(jìn)一步的初始化。如果需要對(duì)對(duì)象進(jìn)行自定義的初始化操作,可以使用結(jié)構(gòu)體字面量或構(gòu)造函數(shù)等方式
到此這篇關(guān)于Golang實(shí)現(xiàn)短網(wǎng)址/短鏈服務(wù)的開(kāi)發(fā)筆記分享的文章就介紹到這了,更多相關(guān)Golang短網(wǎng)址內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang?beego框架環(huán)境搭建過(guò)程
這篇文章主要為大家介紹了golang?beego框架環(huán)境搭建的過(guò)程腳本,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04
Go語(yǔ)言轉(zhuǎn)換所有字符串為大寫(xiě)或者小寫(xiě)的方法
這篇文章主要介紹了Go語(yǔ)言轉(zhuǎn)換所有字符串為大寫(xiě)或者小寫(xiě)的方法,實(shí)例分析了ToLower和ToUpper函數(shù)的使用技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-02-02
詳解Go并發(fā)編程時(shí)如何避免發(fā)生競(jìng)態(tài)條件和數(shù)據(jù)競(jìng)爭(zhēng)
大家都知道,Go是一種支持并發(fā)編程的編程語(yǔ)言,但并發(fā)編程也是比較復(fù)雜和容易出錯(cuò)的。比如本篇分享的問(wèn)題:競(jìng)態(tài)條件和數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題2023-04-04
go使用SQLX操作MySQL數(shù)據(jù)庫(kù)的教程詳解
sqlx 是 Go 語(yǔ)言中一個(gè)流行的操作數(shù)據(jù)庫(kù)的第三方包,它提供了對(duì) Go 標(biāo)準(zhǔn)庫(kù) database/sql 的擴(kuò)展,簡(jiǎn)化了操作數(shù)據(jù)庫(kù)的步驟,下面我們就來(lái)學(xué)習(xí)一下go如何使用SQLX實(shí)現(xiàn)MySQL數(shù)據(jù)庫(kù)的一些基本操作吧2023-11-11
golang類(lèi)型推斷與隱式類(lèi)型轉(zhuǎn)換
這篇文章主要介紹了golang類(lèi)型推斷與隱式類(lèi)型轉(zhuǎn)換,golang類(lèi)型推斷可以省略類(lèi)型,像寫(xiě)動(dòng)態(tài)語(yǔ)言代碼一樣,讓編程變得更加簡(jiǎn)單,同時(shí)也保留了靜態(tài)類(lèi)型的安全性2022-06-06

