这篇文章是根据课程 Go 开发短地址服务 做的笔记

项目地址:github.com/astak16/sho…

过错处理

在处理业务逻辑时,如果出过错了,需求一致处理过错呼应的格式,这样能够便利前端处理过错信息

所以需求界说一个 Error 接口,它包含了 error 接口,以及一个 Status() 办法,用来回来过错的状况码

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 中 完成 Error 接口,只需求完成 Error()Status() 办法即可

func () Error() string {
  return ""
}
func () Status() int {
  return 0
}

这样界说的办法,只能回来固定的文本和状况码,如果想要回来动态内容,能够界说一个结构体

然后 ErrorStatus 办法承受 StatusError 类型

这样只需满足 StatusError 类型的结构体,就能够回来动态内容

所以上面的代码能够修改为:

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 状况码

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

中间件运用

alicego 中的一个中间件库,能够经过 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,也就是说当外面传入 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 有两种情况:

  1. redis.Nil 表明没有找到对应的值
  2. 其他过错,表明 redis 服务出错了

所以在运用 redis 时,需求判别回来的过错类型

if err == redis.Nil {
  // 没有找到对应的值
} else if err != nil {
  // redis 服务出错了
} else {
  // 正确呼应
}

测验

在测验用例中,如何建议一个恳求,然后获取呼应的数据呢?

  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")
  1. 捕获 http 呼应
rw := httptest.NewRecorder()
  1. 模拟恳求被处理
app.Router.ServeHTTP(rw, req)
  1. 解析呼应
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 情况
    }
  }
}

需求留意的是:

  1. recover 函数只能在 defer 中运用,如果在 defer 之外运用,会直接回来 nil
  2. recover 函数只要在 panic 之后调用才会生效,如果在 panic 之前调用,也会直接回来 nil
  3. recover 函数只能捕获当前 goroutinepanic,不能捕获其他 goroutinepanic

next.ServerHttp(w, r)

next.ServeHTTP(w, r),用于将 http 恳求传递给下一个 handler

HandleFunc 和 Handle 差异

HandleFunc 承受一个一般类型的函数:

func myHandle(w http.ResponseWriter, r *http.Request) {}
http.HandleFunc("xxxx", myHandle)

Handle 接纳一个完成 Handler 接口的函数:

func myHandler(w http.ResponseWriter, r *http.Request) {}
http.Handle("xxxx", http.HandlerFunc(myHandler))

他们的差异是:运用 Handle 需求自己进行包装,运用 HandleFunc 不需求

defer res.Body.Close()

为什么没有 res.Header.Close() 办法?

由于 header 不是资源,而 body 是资源,在 go 中,一般操作资源后,要及时关闭资源,所以 gobody 供给了 Close() 办法

res.Bodyio.ReadCloser 类型的接口,表明能够读取呼应数据并关闭呼应体的目标

w.Write()

代码在履行了 w.Writer(res) 后,还会持续往下履行,除非有显现的 returepanic 停止函数履行

func controller(w http.ResponseWriter, r *http.Request) {
  if res, err := xxx; err != nil {
    respondWithJSON(w, http.StatusOK, err)
  }
  // 这里如果有代码,会持续履行
}
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)
}

需求留意的是,虽然履行了 w.Writer() 后,还会持续往下履行,但不会再对呼应进行修改或写入任何内容了,由于 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 类型,它代表恳求的原始数据

如果关联成功能够用 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

用于创立一个新的零值目标,并回来该目标的指针

它承受一个类型作为参数,并回来一个指向该类型的指针

适用于任何可分配的类型,如基本类型、结构体、数组、切片、映射和接口等

// 创立一个新的 int 类型的零值目标,并回来指向它的指针
ptr := new(int)  // 0

需求留意的是:new 只分配了内存,并初始化为零值,并不会对目标进行任何进一步的初始化。如果需求对目标进行自界说的初始化操作,能够运用结构体字面量或结构函数等方法

往期文章

  1. Go 项目ORM、测验、api文档建立
  2. Go 完成一致加载资源的入口
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。