【慕课网】Go开发短地址服务
1 短地址服务
将长地址缩短到一个很短的地址,用户访问这个短地址可以重定向到原本的长地址。
- 如何设计HTTP Router 和handler
- 如何在HTTP 处理流程中加入Middleware
- 如何利用Go的Interface来实现可扩展的设计
- 如何使用redis的自增长序列生成短地址
2 主服务模块
API接口
- POST /api/shorten
- GET /api/info?shortlink=shortlink
- GET /:shortlink - return 302 code 重定向
POST /api/shorten
-
Params
Name Type Description url string Required. URL to shorten. e.g. https://www.example.com expiration_in_minutes int Required. Expiration of short link in minutes. e.g. value 0 represents permanent. -
Response
1{ 2 "shortlink":"P" 3}
GET /api/info?shortlink=shortlink
-
Params
Name Type Description shortlink string Required. Id of shortened. e.g. P -
Response
1{ 2 "url":"https://www.example.com", 3 "created_at": "2022-03-09 23:07:03", 4 "expiration_in_minutes": 60 5}
GET /:shortlink - return 302 code 重定向 Redirect
302 临时重定向;301会永久保存在用户的缓存中。
一个完整的HTTP 处理流程
Middleware 是一个pipeline:认证,鉴权,log
Mux中router和Handler设计
gorilla/mux 处理router 和handler
1func main(){
2 r := mux.NewRouter()
3 r.HandleFunc("/", HomeHandler)
4 r.HandleFunc("/product", ProductsHandler)
5 r.HandleFunc("/articles",ArticlesHandler)
6 http.Handle("/",r)
7}
实现router和handler
工程代码
1➜ goshorten tree
2.
3├── app.go
4└── main.go
main.go
1package main
2
3func main() {
4 a := App{}
5 a.Initialize()
6 a.Run(":8000")
7}
app.go
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "log"
8 "net/http"
9
10 "github.com/gorilla/mux"
11 "gopkg.in/validator.v2"
12)
13
14// App encapsulate Env, Router and middleware
15type App struct {
16 Router *mux.Router
17}
18
19type shortenReq struct {
20 URL string `json:"url" validate:"nonzero"`
21 ExpirationInMinutes int64 `json:"expiration_in_minutes" validate:"min=0"`
22}
23
24type shortlinkResp struct {
25 ShortLink string `json:"shortlink"`
26}
27
28// Initialize is initialization of app
29func (a *App) Initialize() {
30 // set log formatter
31 log.SetFlags(log.LstdFlags | log.Lshortfile)
32 a.Router = mux.NewRouter()
33 a.initializeRouters()
34}
35
36func (a *App) initializeRouters() {
37
38 a.Router.HandleFunc("/api/shorten", a.createShortLink).Methods("POST")
39 a.Router.HandleFunc("/api/info", a.getShortLinkInfo).Methods("GET")
40 a.Router.HandleFunc("/{shortlink:[a-zA-Z0-9]{1,11}}", a.redirect).Methods("GET")
41}
42
43func (a *App) createShortLink(w http.ResponseWriter, r *http.Request) {
44 var req shortenReq
45 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
46 return
47 }
48 if err := validator.Validate(req); err != nil {
49 return
50 }
51 defer func(Body io.ReadCloser) {
52 _ = Body.Close()
53 }(r.Body)
54
55 fmt.Printf("%v\n", req)
56}
57
58func (a *App) getShortLinkInfo(w http.ResponseWriter, r *http.Request) {
59
60 vars := r.URL.Query()
61 s := vars.Get("shortlink")
62
63 fmt.Printf("%s\n", s)
64}
65
66func (a *App) redirect(w http.ResponseWriter, r *http.Request) {
67
68 vars := mux.Vars(r)
69 fmt.Printf("%s\n", vars["shortlink"])
70}
71
72// Run starts listen and serve
73func (a *App) Run(addr string) {
74 log.Fatal(http.ListenAndServe(addr, a.Router))
75}
测试API
1curl -X POST \
2 http://localhost:8000/api/shorten \
3 -d '{"url":"www.baidu.com","expiration_in_minutes":1}'
4
5curl -X GET \
6 'http://localhost:8000/api/info?shortlink=hi'
7
8curl -X GET \
9 http://localhost:8000/A
错误处理设计
-
An interface type is defined as a set of method signatures.
一个接口是一系列方法签名的集合
-
A values of interface type can hold any value that implements those methods.
一个接口的类型 可以接受任何实现了接口方法的对象
1// Go 的内置接口
2type error interface{
3 Error() string
4}
1err := errors.New("Error message!")
2if err != nil{
3 fmt.Print(err)
4}
工程代码
1.
2├── app.go
3├── error.go
4└── main.go
error.go
1package main
2
3type Error interface {
4 error
5 Status() int
6}
7
8type StatusError struct {
9 Code int
10 Err error
11}
12
13func (se StatusError) Error() string { // (se *StatusError) 区别
14 return se.Err.Error()
15}
16
17func (se StatusError) Status() int {
18 return se.Code
19}
app.go
1func (a *App) createShortLink(w http.ResponseWriter, r *http.Request) {
2 var req shortenReq
3 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
4 responseWithError(w, StatusError{http.StatusBadRequest,
5 fmt.Errorf("prase parameters failed %v", r.Body)})
6 return
7 }
8 if err := validator.Validate(req); err != nil {
9 responseWithError(w, StatusError{http.StatusBadRequest,
10 fmt.Errorf("validate parameters failed %v", req)})
11 return
12 }
13 defer func(Body io.ReadCloser) {
14 _ = Body.Close()
15 }(r.Body)
16
17 fmt.Printf("%v\n", req)
18}
19
20func responseWithError(w http.ResponseWriter, err error) {
21 switch e := err.(type) {
22 case Error:
23 log.Printf("HTTP %d - %s", e.Status(), e)
24 responseWithJSON(w, e.Status(), e.Error())
25 default:
26 responseWithJSON(w, http.StatusInternalServerError,
27 http.StatusText(http.StatusInternalServerError))
28 }
29
30}
31
32func responseWithJSON(w http.ResponseWriter, code int, payload interface{}) {
33 resp, _ := json.Marshal(payload)
34 w.Header().Set("Content-Type", "application/json")
35 w.WriteHeader(code)
36 _, _ = w.Write(resp)
37}
3 中间件模块 Middleware
-
Log Middleware
-
Recover Middleware
middleware.go
1package main
2
3import (
4 "log"
5 "net/http"
6 "time"
7)
8
9type Middleware struct {
10}
11
12//LoggingHandler log the time-consuming of http request
13func (m Middleware) LoggingHandler(next http.Handler) http.Handler {
14 fn := func(w http.ResponseWriter, r *http.Request) {
15 t1 := time.Now()
16 next.ServeHTTP(w, r)
17 t2 := time.Now()
18 log.Printf("[%s] %q %v", r.Method, r.URL.String(), t2.Sub(t1))
19 }
20
21 // adaptor
22 return http.HandlerFunc(fn)
23}
24
25// RecoverHandler recover panic
26func (m Middleware) RecoverHandler(next http.Handler) http.Handler {
27 fn := func(w http.ResponseWriter, r *http.Request) {
28 defer func() {
29 if err := recover(); err != nil {
30 log.Printf("Recover from panic: %+v", err)
31 http.Error(w, http.StatusText(500), 500)
32 }
33 }()
34 next.ServeHTTP(w, r)
35 }
36 return http.HandlerFunc(fn)
37}
Alice包的使用
- Alice provide a convenient way to chain your HTTP middleware function and the app handler.
1Middleware1(Middlerware2(Middlerware3(app)))
2
3alice.New(Middlerware1, Middleware2, Middleware3).Then(app)
4 存储模块 Storage
如何生成短地址
- INCR key
1redis> SET mykey "10"
2"OK"
3redis>INCR mykey
4(integer) 11
5redis>get mykey
6"11"
7redis>
Redis 客户端
Medis TablePlus
Redis-cli
Storage接口设计
实现Shorten, Unshort 和 ShortlinkInfo 接口
1curl -X POST \
2 http://localhost:8000/api/shorten \
3 -d '{"url":"www.baidu.com","expiration_in_minutes":15}'
4
5curl -X GET \
6 'http://localhost:8000/api/info?short_link=8'
7
8curl -X GET \
9 http://localhost:8000/2
5 单元测试
主服务测试用例