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}
    
  • 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}
    

302 临时重定向;301会永久保存在用户的缓存中。

一个完整的HTTP 处理流程

image-20220309230958064

Middleware 是一个pipeline:认证,鉴权,log

Mux中router和Handler设计

gorilla/mux 处理router 和handler

image-20191226200615163
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

image-20191227145907981

  • 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

如何生成短地址

image-20191227155742062
  • 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接口设计

image-20191227160110874

实现Shorten, Unshort 和 ShortlinkInfo 接口

image-20200103164939939

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 单元测试

主服务测试用例