单元测试

什么是单元测试

  • 首先,它是测试
  • 其次,它是单元测试
  • 举个栗子

实现一个加法函数

func Add(x, y int) int {
   return x + y
}

要测试函数是否正确,我们可能会写下这坨代码

func TestAdd() {
   type TCase struct {
      x, y, ret int
   }
   cases := make([]TCase, 0, 0)
   // 第一个 Case
   cases = append(cases, TCase{
      x:   1,
      y:   2,
      ret: 3,
   })
   // 第二个 Case
   cases = append(cases, TCase{
      x:   -1,
      y:   1,
      ret: 0,
   })
   for _, c := range cases {
      got := Add(c.x, c.y)
      if got != c.ret {
         log.Printf("%d+%d, got %d, but %d expected", c.x, c.y, got, c.ret)
      }
   }
}

为什么需要单元测试

  • 函数绝对不会有 Bug,覆盖率 100% 了(正确性)
  • 总有一天,你会遇到重构(重构)
  • 钝角(其他难以言表)

哪里需要单元测试

  • 我就加了一个 if,怎么就出 bug 了(复杂&&容易出错)
  • 这个库整个公司都在使用,出问题就 n+1 了(基础代码、公共代码)
  • 这个广告计费模块太重要了(重要性)
  • 这个需求老变,每次一变我就要改好多单测(基础、底层)

什么时候写单元测试

  • 宇宙的尽头是 TDD(编码前)
  • 一步一个脚印(编码中)
  • 偷偷补上,然后惊艳所有人(编码后)

如何写单元测试

在当前 package 下新建文件add.go,并实现 Add 函数

// add.go
package main

func Add(x, y int) int {
   return x + y
}

我们想测试 Add 函数,那么我们需要在 add.go 的 package 下创建 add_test.go,并实现逻辑

// add_test.go
package main

func TestAdd1(t *testing.T) {
   x := 1
   y := 1
   want := 2
   got := Add(x, y)
   if got != want {
      t.Errorf("%d+%d=%d, but %d expected", x, y, got, want)
   }
}

如果我们想并发跑很多 Case

// add_test.go
func TestAdd2(t *testing.T) {
   type tCase struct {
      x        int
      y        int
      expected int
   }
   cases := make([]*tCase, 0, 0)

   // test1
   cases = append(cases, &tCase{
      x:        1,
      y:        -1,
      expected: 0,
   })

   // test2
   cases = append(cases, &tCase{
      x:        1,
      y:        2,
      expected: 3,
   })
   for i, c := range cases {
      t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) {
         got := Add(c.x, c.y)
         if got != c.expected {
            t.Errorf("%d+%d expected %d, but %d got", c.x, c.y, c.expected, got)
         }
      })
   }
}

mock 必不可少

// add_test.go
func init() {
   rand.Seed(time.Now().UnixNano())
}

func AddWithRandom(x, y int) int {
   return x + y + rand.Int()
}

mock random.Int()

func TestAddWithRandom(t *testing.T) {
   monkey.Patch(rand.Int, func() int { return 0 })
   x := 1
   y := 1
   expected := 2
   got := AddWithRandom(x, y)
   if got != expected {
      t.Errorf("%d+%d expected %d, but %d got", x, y, expected, got)
   }
}

基准测试

func BenchmarkAdd(b *testing.B) {
   for i := 0; i < b.N; i++ {
      for j := 0; j < 1e9; j++ {
         Add(i, i+1)
      }
   }
}

func BenchmarkMul(b *testing.B) {
   for i := 0; i < b.N; i++ {
      for j := 0; j < 1e9; j++ {
         Mul(i, i+1)
      }
   }
}

Gin 框架

什么是框架

先举一个简单例子,比如小 A 让我实现 2^n % 123456789,我三下五除二搞定了

func main() {
   var n int
   fmt.Scanf("%d", &n)
   mod := 123456789
   x := 2
   ans := 1
   for ; n > 0; n >>= 1 {
      if n&1 != 0 {
         ans = ans * x % mod
      }
      x = x * x % mod
   }
   fmt.Printf("%d\n", ans)
}

(函数)过了几天,小 B 又让我实现 5^n % 6789,这时候我有两种方法,一种是 copy 代码改一改,一种是抽象一下,写一个 pow 函数,防止之后有小 C、 小 D 又有类似的需求

func pow(x, n, mod int) (ans int) {
   ans = 1
   for ; n > 0; n >>= 1 {
      if n&1 != 0 {
         ans = ans * x % mod
      }
      x = x * x % mod
   }
   return
}

(库)也不知道为什么,产品喜欢上了求幂,不仅给我提了需求,还给了研发小 E 提了类似的需求。小 E 以一顿饭的报酬想要拷贝我的代码,我怎么会同意这种做法,拷贝代码太低级了,我说:饭就不用了,我搞个库给你。

(框架)产品为了让大家更好的理解框架,提了一个惨绝人寰的需求,需求如下

  • 输入一个整数 n(HTTP 请求)
  • 斐波那契数列第 n 项为 x(建立 TCP 连接)
  • y = n ^ x % 1993(解析 HTTP 协议)
  • 根据 y 的特性,进行处理,这个处理是经常变化的(业务逻辑)
    • 例如1:如果 y 是奇数,返回 hellow,如果 y 是偶数,返回 world
    • 例如2:如果 y 大于 1000 则巴拉巴拉,否则巴拉巴拉

简单地理解

  • 函数:代码复用
  • 库:跨项目的代码复用
  • 框架:本身也是一个库,可能由多个库组装而成,形成一个解决方案,开发者只需要关注业务逻辑,本质也是代码的复用

HTTP 协议

什么是协议

数据+约定

什么是 HTTP 协议

  • 应用层协议
  • HTTP Header
    • Method:GET、POST、PUT、PATCH、DELETE
    • Content-Type:application/json、text/html、multipart/form-data
    • Status Code:2xx,3xx,4xx,5xx
    • Cookie:
  • Ajax
  • Version 1.0、1.1、2.0、3.0

Gin 框架的使用

Path + Method -> Handler

func main() {
        // Creates a gin router with default middleware:
        // logger and recovery (crash-free) middleware
        router := gin.Default()

        router.GET("/someGet", getting)
        router.POST("/somePost", posting)
        router.PUT("/somePut", putting)
        router.DELETE("/someDelete", deleting)
        router.PATCH("/somePatch", patching)
        router.HEAD("/someHead", head)
        router.OPTIONS("/someOptions", options)

        // By default it serves on :8080 unless a
        // PORT environment variable was defined.
        router.Run()
        // router.Run(":3000") for a hard coded port
}

Validation -> Binding

type Login struct {
        User     string `form:"user" json:"user" xml:"user"  binding:"required"`
        Password string `form:"password" json:"password" xml:"password" binding:"required"`
}

func main() {
        router := gin.Default()

        // Example for binding JSON ({"user": "manu", "password": "123"})
        router.POST("/loginJSON", func(c *gin.Context) {
                var json Login
                if err := c.ShouldBindJSON(&json); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                        return
                }

                if json.User != "manu" || json.Password != "123" {
                        c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
                        return
                }

                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
        })

        // Example for binding XML (
        //        <?xml version="1.0" encoding="UTF-8"?>
        //        <root>
        //                <user>manu</user>
        //                <password>123</password>
        //        </root>)
        router.POST("/loginXML", func(c *gin.Context) {
                var xml Login
                if err := c.ShouldBindXML(&xml); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                        return
                }

                if xml.User != "manu" || xml.Password != "123" {
                        c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
                        return
                }

                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
        })

        // Example for binding a HTML form (user=manu&password=123)
        router.POST("/loginForm", func(c *gin.Context) {
                var form Login
                // This will infer what binder to use depending on the content-type header.
                if err := c.ShouldBind(&form); err != nil {
                        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
                        return
                }

                if form.User != "manu" || form.Password != "123" {
                        c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
                        return
                }

                c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
        })

        // Listen and serve on 0.0.0.0:8080
        router.Run(":8080")
}

Gorm 框架

什么是数据库

  • 存储 + CURD
  • 快:索引

什么是关系型数据库

举个例子,老师课表

CREATE TABLE `teacher_schedule` (
        `id` INT UNSIGNED AUTO_INCREMENT COMMENT "ID",
        `teacher_id` INT COMMENT "老师 ID",
        `course_id` INT COMMENT "课程 ID",
        `time` TIMESTAMP COMMENT "上课时间",
        PRIMARY KEY(`id`),
        KEY(`teacher_id`),
        KEY(`course_id`)
);
CREATE TABLE `teacher` (
        `id` INT UNSIGNED AUTO_INCREMENT COMMENT "ID",
        `name` VARCHAR(255) COMMENT "老师姓名",
        PRIMARY KEY(`id`)
);
CREATE TABLE `course` (
        `id` INT UNSIGNED AUTO_INCREMENT COMMENT "ID",
        `name` VARCHAR(255) COMMENT "课程名称",
        PRIMARY KEY(`id`)
)
老师 ID 老师姓名
1 张三
2 李四
课程 ID 课程名称
1 语文
2 数据
时间 老师 ID 课程 ID
1月25日早8点 1 1
1月25日早10点 2 1
1月25日中午14点 1 2

什么是索引

各种数据结构,就是为了更快地检索到数据

  • 查找树:B+、B
  • Hash

什么是 ORM

  • Struct -> Table
  • Object -> Raw
  • Filed -> Column

Gorm 的使用

Declaring Models

type Teacher struct {
   ID   int
   Name string
}

func (Teacher) TableName() string {
   return "teacher"
}

type Course struct {
   ID   int
   Name string
}

func (Course) TableName() string {
   return "course"
}

type TeacherSchedule struct {
   ID        int
   TeacherID int
   CourseID  int
}

func (TeacherSchedule) TableName() string {
   return "teacher_schedule"
}

Connect

package main

import (
   "fmt"
   "testing"

   "gorm.io/driver/mysql"
   "gorm.io/gorm"
)

func main() {
   dsn := "root:bytedancecamp@tcp(180.184.74.182:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
   db, err := gorm.Open(mysql.Open(dsn))
   if err != nil {
      panic(fmt.Sprintf("open mysql failed, err is %s", err))
   }
   // do something with db
}

CURD(Create、Update、Read、Delete)

func main() {
   dsn := "root:bytedancecamp@tcp(180.184.74.182:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
   db, err := gorm.Open(mysql.Open(dsn))
   db = db.Debug()
   if err != nil {
      panic(fmt.Sprintf("open mysql failed, err is %s", err))
   }
   // create
   teacher := &Teacher{
      Name: "张三",
   }
   if err := db.Create(teacher).Error; err != nil {
      panic(fmt.Sprintf("create failed, err is %s", err))
   }
   // update
   teacher.Name = "李四"
   if err := db.Where("name = ?", "张三").Updates(teacher).Error; err != nil {
      panic(fmt.Sprintf("update failed, err is %s", err))
   }
   // query
   teacher2 := new(Teacher)
   if err := db.First(teacher2, "name = ?", "李四").Error; err != nil {
      panic(fmt.Sprintf("query failed ,err is %s", err))
   }
   // delete
   if err := db.Delete(&Teacher{}, "name = ?", "李四").Error; err != nil {
      panic(fmt.Sprintf("delete failed, err is %s", err))
   }
}

[176.107ms] [rows:1] INSERT INTO `teacher` (`name`) VALUES ('张三')

[173.249ms] [rows:1] UPDATE `teacher` SET `name`='李四' WHERE name = '张三' AND `id` = 2

[86.831ms] [rows:1] SELECT * FROM `teacher` WHERE name = '李四' ORDER BY `teacher`.`id` LIMIT 1

[173.992ms] [rows:1] DELETE FROM `teacher` WHERE name = '李四'

软删除

初识微服务

什么是微服务

  • 单体 -> 微服务,分工
  • RPC

为什么需要微服务

  • 效率、分工、沟通成本、交易成本、人月神话
  • 康威定律
  • 领域驱动设计
  • 部署、复杂度

服务治理

  • 服务注册、发现
  • 限流、熔断
  • 日志采集
  • 链路追踪