#55 Golang Redis

2021-06-05
package main

import (
    "log"
    "time"

    "github.com/go-redis/redis"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    var err error

    // 使用 Get/Set/Del =====================================

    err = client.Set("key", "value", 0).Err()
    if err != nil {
        panic(err)
    }

    val, err := client.Get("key").Result()
    if err != nil {
        panic(err)
    }
    log.Println("key", val)

    err = client.Del("key").Err()
    if err != nil {
        panic(err)
    }

    // 使用 Pipeline ========================================

    pipeline := client.Pipeline()

    pipeline.Set("key1", "value1", time.Minute*5)
    pipeline.Set("key2", "value2", time.Minute*5)
    pipeline.Set("key3", "value3", time.Minute*5)
    _, err = pipeline.Exec()
    if err != nil {
        panic(err)
    }

    pipeline.Get("key1")
    pipeline.Get("key2")
    pipeline.Get("key3")
    vals, err := pipeline.Exec()
    if err != nil {
        panic(err)
    }
    val1, _ := vals[0].(*redis.StringCmd).Result()
    val2, _ := vals[1].(*redis.StringCmd).Result()
    val3, _ := vals[2].(*redis.StringCmd).Result()
    log.Println("key1", val1)
    log.Println("key2", val2)
    log.Println("key3", val3)

    pipeline.Del("key1", "key2", "key3")
    _, err = pipeline.Exec()
    if err != nil {
        panic(err)
    }
}
package main

import (
    "log"

    "github.com/go-redis/redis"
)

func main() {
    client := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })
    log.Println(client)

    var err error
    var items []string

    // List ===============================================
    {
        err = client.RPush("mylist", "item1", "item2", "item3").Err()
        if err != nil {
            panic(err)
        }
        listLen, err := client.LLen("mylist").Result()
        if err != nil {
            panic(err)
        }
        log.Println("Length of mylist:", listLen)
        items, err = client.LRange("mylist", 0, -1).Result()
        if err != nil {
            panic(err)
        }
        log.Println("===== Items in mylist:", items)
    }

    // Hash ===============================================

    {
        err = client.HSet("myhash", "field1", "value1").Err()
        if err != nil {
            panic(err)
        }
        value1, err := client.HGet("myhash", "field1").Result()
        if err != nil {
            panic(err)
        }
        log.Println("Value of field1:", value1)
        allFields, err := client.HGetAll("myhash").Result()
        if err != nil {
            panic(err)
        }
        log.Println("===== All fields in myhash:", allFields)
    }
    // Set ================================================

    {
        err = client.SAdd("myset", "item1", "item2", "item3").Err()
        if err != nil {
            panic(err)
        }
        setLen, err := client.SCard("myset").Result()
        if err != nil {
            panic(err)
        }
        log.Println("Length of myset:", setLen)
        items, err = client.SMembers("myset").Result()
        if err != nil {
            panic(err)
        }
        log.Println("===== Items in myset:", items)
    }
    // ZSet ===============================================
    {
        err = client.ZAdd("myzset", redis.Z{Score: 1.0, Member: "one"}, redis.Z{Score: 2.0, Member: "two"}).Err()
        if err != nil {
            panic(err)
        }
        setLen, err := client.ZCard("myzset").Result()
        if err != nil {
            panic(err)
        }
        log.Println("Length of myzset:", setLen)
        items, err = client.ZRange("myzset", 0, -1).Result()
        if err != nil {
            panic(err)
        }
        log.Println("===== Items in myzset:", items)
    }
}

哨兵

这样理论上来说,肯定会有性能损耗,毕竟增加了和哨兵的通信。
具体能差多少,还得实验。

package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
)

func main() {
    failoverClient := redis.NewFailoverClient(&redis.FailoverOptions{
        SentinelAddrs: []string{"sentinel1:26379", "sentinel2:26379", "sentinel3:26379"},
        MasterName:    "mymaster",
    })

    pong, err := failoverClient.Ping().Result()
    if err != nil {
        panic(err)
    }
    fmt.Println(pong)
}

集群

package main

import (
    "fmt"
    "github.com/go-redis/redis/v8"
)

func main() {
    clusterClient := redis.NewClusterClient(&redis.ClusterOptions{
        Addrs: []string{"redis1:6379", "redis2:6379", "redis3:6379"},
    })
    pong, err := clusterClient.Ping().Result()
    if err != nil {
        panic(err)
    }
    fmt.Println(pong)
}

#54 Golang MySQL

2021-06-04

测试表:

CREATE TABLE `users` (
    `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    `password` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    `nickname` VARCHAR(50) NOT NULL COLLATE 'utf8mb4_unicode_ci',
    `email` VARCHAR(100) NOT NULL DEFAULT '' COLLATE 'utf8mb4_unicode_ci',
    `birthday` VARCHAR(10) NOT NULL DEFAULT '0000-00-00' COLLATE 'utf8mb4_unicode_ci',
    `age` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
    `level` TINYINT(3) NOT NULL DEFAULT '0',
    `disabled` TINYINT(1) NOT NULL DEFAULT '0',
    `created_at` DATETIME NOT NULL DEFAULT current_timestamp(),
    `updated_at` DATETIME NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
    PRIMARY KEY (`id`) USING BTREE,
    UNIQUE INDEX `username` (`username`) USING BTREE
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB;

连接

package main

import (
 "database/sql"
 "fmt"
 "time"

 _ "github.com/go-sql-driver/mysql"
)

var db *sql.DB   //全局变量client

func initMySQL() (err error) {
 dsn := "root:123456@tcp(127.0.0.1:3306)/test"
 db, err = sql.Open("mysql", dsn)
 if err != nil {
  panic(err)
 }
 err = db.Ping() //检测是否连接成功
 if err != nil {
  return
 }
 db.SetMaxOpenConns(200)                 //最大连接数
 db.SetMaxIdleConns(10)                  //连接池里最大空闲连接数。必须要比maxOpenConns小
 db.SetConnMaxLifetime(time.Second * 10) //最大存活保持时间
 db.SetConnMaxIdleTime(time.Second * 10) //最大空闲保持时间
 return
}

func main() {
 if err := initMySQL(); err != nil {
  fmt.Printf("connect to db failed,err:%v\n", err)
 } else {
  fmt.Println("connect to db success")
 }

 sqlStr := "SELECT id, name FROM sys_user WHERE id=?"
 var u user
 //非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
 err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name)
 if err != nil {
  fmt.Printf("scan failed, err: %v\n", err)
  return
 }
 fmt.Printf("id:%d,name:%s,age:%d\n", u.id, u.name)

 defer db.Close()

}

//user结构体
type user struct {
 id int
 name string
}

Insert 增

Update 改

Delete 删

Select 查

#53 Go interface

2021-06-02

不像我们常见的一些语言,实现接口需要显式声明,比如 PHP:

interface Animal {
    public function move();
    public function makeSound();
}

class Dog implements Animal {
    public function move() {
        echo "Dog is moving\n";
    }

    public function makeSound() {
        echo "Woof\n";
    }
}

Go 不需要声明,说到不如做到,实现了所有接口方法就行了。
PS:我还是觉得写出来比较好一些,阅读代码的时候更加一目了然,也没有什么副作用。

示例

package main

import "fmt"

type Animal interface {
    move() string
    makeSound() string
}

type Dog struct {
    name string
}

func (d Dog) move() string {
    return fmt.Sprintf("%s is running", d.name)
}

func (d Dog) makeSound() string {
    return "woof"
}

func main() {
    dog := Dog{name: "Fido"}

    // 判断接口实现关系
    _, ok := interface{}(dog).(Animal)
    if ok {
        fmt.Printf("%T is Animal\n", dog)
    } else {
        fmt.Printf("%T is NOT Animal\n", dog)
    }

    fmt.Println(dog.move())
    fmt.Println(dog.makeSound())
}

interface{}

interface{} says nothing.

Go 谚语第六条是什么意思?

我的理解:

interface{} 可以表示任意类型,有时使用起来会很方便。
但是必须注意这意味着对类型没有任何约束,无论从程序的严谨度还是代码可读性上讲,都是有害的。
如何可以的话,编码中应当尽量采用范围小一些的接口,就比如到处可见的 io.Writer 接口,它有各类实现:

  • bytes.Buffer
  • bufio.Writer
  • os.File
  • compress/gzip.Writer
  • crypto/cipher.StreamWriter
  • encoding/csv.Writer
  • net/http.ResponseWriter
  • archive/zip.Writer
  • image/png.Encoder

#52 Golang JSON

2021-06-02

Golang 有一个 encoding/json 标准库,在多数情况下已经够用了。

注意:因为 Golang 是一种静态类型的语言,所以解析 JSON 字符串必须了解里面的数据结构。
如果是用惯了 JS、Python、PHP 等动态语言的话,到这里会有些郁闷,怎么解析一个 JSON 还这么复杂。

#51 Gron 的设计

2021-05-30

2021/08/23,Golang 定时任务 是讲通过 Golang 原生的 time.Ticker 实现定时任务,本文介绍的是通过第三方库 robfig/cron 实现定时任务。

概述

  • 代码:robfig/cron
  • 文档: https://godoc.org/github.com/robfig/cron

标准 Cron 表达式 分成 5 段:分 minutes、时 hours、日 day of month、月 month、周 day of week。
robfig/cron v2 默认支持 6 个字段(包括秒 seconds),从 v3 开始又回到默认 5 个字段(需要做以下显式声明:cron.New(cron.WithSeconds()))。

type SpecSchedule struct {
    Second, Minute, Hour, Dom, Month, Dow uint64
}

使用

// Seconds field, required
cron.New(cron.WithSeconds())

// Seconds field, optional
cron.New(cron.WithParser(cron.NewParser(
    cron.SecondOptional | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor,
)))
c := cron.New()
c.AddFunc("0 30 * * * *", func() { fmt.Println("Every hour on the half hour") })
c.AddFunc("@hourly",      func() { fmt.Println("Every hour") })
c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })
c.Start()
..
// Funcs are invoked in their own goroutine, asynchronously.
...
// Funcs may also be added to a running Cron
c.AddFunc("@daily", func() { fmt.Println("Every day") })
..
// Inspect the cron job entries' next and previous run times.
inspect(c.Entries())
..
c.Stop()  // Stop the scheduler (does not stop any jobs already running).
Field name   | Mandatory? | Allowed values  | Allowed special characters
----------   | ---------- | --------------  | --------------------------
Seconds      | Yes        | 0-59            | * / , -
Minutes      | Yes        | 0-59            | * / , -
Hours        | Yes        | 0-23            | * / , -
Day of month | Yes        | 1-31            | * / , - ?
Month        | Yes        | 1-12 or JAN-DEC | * / , -
Day of week  | Yes        | 0-6 or SUN-SAT  | * / , - ?

Entry                  | Description                                | Equivalent To
-----                  | -----------                                | -------------
@yearly (or @annually) | Run once a year, midnight, Jan. 1st        | 0 0 0 1 1 *
@monthly               | Run once a month, midnight, first of month | 0 0 0 1 * *
@weekly                | Run once a week, midnight between Sat/Sun  | 0 0 0 * * 0
@daily (or @midnight)  | Run once a day, midnight                   | 0 0 0 * * *
@hourly                | Run once an hour, beginning of hour        | 0 0 * * * *

#50 Golang 时间处理

2021-05-21

主要是 time 包(2021/05/21,Golang time)。

  • time.Time 时间,精确到纳秒
  • time.Location 时区
  • time.Duration 时间间隔,精确到纳秒,[-292.47 years, 292.47 years]
 (1 << 63) / 1_000_000_000 / 3600 / 24 / 365
292.471208677536
src/time/time.go:type Time struct {
src/time/time.go-       wall uint64     // 秒   wall time seconds
src/time/time.go-       ext  int64      // 毫秒 wall time nanoseconds
src/time/time.go-       loc *Location   // 时区
src/time/time.go-}

src/time/zoneinfo.go:type Location struct {
src/time/zoneinfo.go-   name string
src/time/zoneinfo.go-   zone []zone
src/time/zoneinfo.go-   tx   []zoneTrans
src/time/zoneinfo.go-   extend string
src/time/zoneinfo.go-   cacheStart int64
src/time/zoneinfo.go-   cacheEnd   int64
src/time/zoneinfo.go-   cacheZone  *zone
src/time/zoneinfo.go-}
src/time/zoneinfo.go:type zone struct {
src/time/zoneinfo.go-   name   string // abbreviated name, "CET"
src/time/zoneinfo.go-   offset int    // seconds east of UTC
src/time/zoneinfo.go-   isDST  bool   // is this zone Daylight Savings Time?
src/time/zoneinfo.go-}
src/time/zoneinfo.go-type zoneTrans struct {
src/time/zoneinfo.go-   when         int64 // transition time, in seconds since 1970 GMT
src/time/zoneinfo.go-   index        uint8 // the index of the zone that goes into effect at that time
src/time/zoneinfo.go-   isstd, isutc bool  // ignored - no idea what these mean
src/time/zoneinfo.go-}

var UTC *Location = &utcLoc
var utcLoc = Location{name: "UTC"}
var Local *Location = &localLoc
var localLoc Location

src/time/time.go:type Duration int64

基本用法

time.Now()              // time.Time

// type Month int
// const (
//  January Month = 1 + iota
//  February
//  March
//  April
//  May
//  June
//  July
//  August
//  September
//  October
//  November
//  December
// )
// var Local *Location = &localLoc
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time // 组装 Time

// Time -> 时间戳
time.Now().Unix()       // int64
time.Now().UnixMilli()  // int64 毫秒
time.Now().UnixMicro()  // int64 微秒
time.Now().UnixNano()   // int64 纳秒

func Unix(sec int64, nsec int64) Time   // 时间戳 -> Time

time.Now().Format("2006-01-02 15:04:05")    // Time -> timestr
timeObj, erro := time.Parse("2006-01-02 15:04:05", timestr)  // timestr -> Time
// 特别注意:time.Parse 是按照 UTC 时区解析
timeObj, erro := time.ParseInLocation("2006-01-02 15:04:05", timeStr, time.Local)

间隔计算

func (t Time) Truncate(d Duration) Time
func (t Time) Round(d Duration) Time

func (d Duration) Truncate(m Duration) Duration
func (d Duration) Round(m Duration) Duration

func (t Time) Add(d Duration) Time  // Time 相加
func (t Time) Sub(u Time) Duration  // Time 相减

func Since(t Time) Duration         // Now - t
func Until(t Time) Duration         // t - Now

time.Now().Truncate(时间)
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Hello, 世界")
    now := time.Date(2021, 5, 21, 15, 46, 47, 0, time.Local)
    fmt.Println(now)
    // d := time.Hour
    d := time.Duration(3000_000_000_000)
    fmt.Println(now.Truncate(d))
    fmt.Println(now.Round(d))
}
// 2021-05-21 15:46:47 +0000 UTC
// 2021-05-21 15:00:00 +0000 UTC
// 2021-05-21 15:50:00 +0000 UTC

时区

#46 Gin 框架自学: 基础

2021-05-06

先列一下目前最火的几个 Go 语言框架:

  • gin
    stars num forks num License
    stars
  • beego 国人开发
    stars num forks num License
    stars
  • iris The fastest HTTP/2 Go Web Framework.
    自称是 ExpressJS 和 Laravel 的继承者。
    stars num forks num License
  • echo High performance, minimalist Go web framework
    stars num forks num License
  • go-micro Microservice framework for Go
    stars num forks num License
    stars
  • fiber
    stars num forks num License
  • kratos A Go framework for microservices. B 站出品
    stars num forks num License
  • revel/revel
    stars num forks num License
  • go-zero 国人开发(七牛)
    stars num forks num License
    stars
  • martini Classy web framework for Go
    stars num forks num License
  • GoFrame Classy web framework for Go 国人开发
    stars num forks num License

Gin

http://gin-gonic.com

Gin LOGO

Gin 相比 Beego 非常轻量级,不带 ORM,不支持 Session,正则路由都不支持,需要自行实现。

最新版本 56 个 文件, 算上注释,大概七千行代码。

$ find . -name "*.go" -not -path "*_test.go" | cat | wc -l
56
$ find . -name "*.go" -not -path "*_test.go" | xargs cat | wc -l
6910
Tag CreatedAt ReleasedAt
v1.9.0 2023-02-21 2023-02-21
v1.8.2 2022-12-22 2022-12-22
v1.8.1 2022-06-06 2022-06-06
v1.8.0 2022-05-30 2022-05-30
v1.7.7 2021-11-24 2021-11-24
v1.7.6 2021-08-03 2021-11-23
v1.7.4 2021-08-03 2021-08-15
v1.7.3 2021-08-03 2021-08-03
v1.7.2 2021-05-21 2021-05-21
v1.7.1 2021-04-08 2021-04-08
v1.7.0 2021-04-08 2021-04-08
v1.6.3 2020-05-03 2020-05-03
v1.6.2 2020-03-27 2020-03-27
v1.6.1 2020-03-23 2020-03-23
v1.6.0 2020-03-22 2020-03-22
v1.5.0 2019-11-24 2019-11-28
v1.4.0 2019-05-07 2019-05-08
v1.3.0 2018-08-14 2018-08-14
v1.2 2017-07-02 2017-07-02
v1.1.4 2016-12-04 2016-12-05
v1.1.3 2016-12-03 2016-12-04

HTTP 参数

// c: *gin.Context
// func (c *gin.Context) {
//     balabala...
// }

c.Request.URL.Query()

c.Param(key string) string // 路径参数
c.Params.ByName(key)

c.Query(key string) string // 查询参数
c.Request.URL.Query().Get(key)
c.GetQuery(key)

c.QueryMap(key string) map[string][]string
c.GetQueryMap(key)

c.QueryArray(key string) []string

c.GetQuery(key string) (string, bool)
c.GetQueryArray(key) 的第 0 个元素

c.GetQueryMap(key string) (map[string]string, bool)
c.GetQueryArray(key string) (values []string, ok bool)
c.PostForm(key string) string
c.PostFormMap(key string) map[string][]string
c.PostFormArray(key string) []string
c.GetPostForm(key string) (string, bool)
c.GetPostFormMap(key string) (map[string]string, bool)
c.GetPostFormArray(key string) (values []string, ok bool)
c.FormFile(key string) (*multipart.FileHeader, error)
c.DefaultQuery(key string, defaultValue string) string
c.DefaultPostForm(key string, defaultValue string) string

快速启动

package main

import (
    "io"
    "net/http"
    "os"
    "path/filepath"

    "github.com/gin-gonic/gin"
)

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

    // 处理 GET 请求中的 URL 参数
    r.GET("/hello", func(c *gin.Context) {
        name := c.Query("name")
        c.String(http.StatusOK, "Hello %s!", name)
    })

    // 处理带路径参数的 GET 请求
    r.GET("/users/:id", func(c *gin.Context) {
        id := c.Param("id")
        c.String(http.StatusOK, "User ID: %s", id)
    })

    // 处理 POST 请求中的 JSON 请求体
    r.POST("/data", func(c *gin.Context) {
        var json struct {
            Data string `json:"data"`
        }
        c.BindJSON(&json)
        c.JSON(http.StatusOK, gin.H{"message": "Received data: " + json.Data})
    })

    // 处理文件上传
    r.POST("/upload", func(c *gin.Context) {
        file, err := c.FormFile("file")
        if err != nil {
            c.String(http.StatusBadRequest, "File upload failed: %s", err.Error())
            return
        }

        // 将上传的文件保存到服务器上
        dst := filepath.Join("./uploads", file.Filename)
        if err := c.SaveUploadedFile(file, dst); err != nil {
            c.String(http.StatusInternalServerError, "File save failed: %s", err.Error())
            return
        }

        c.String(http.StatusOK, "File uploaded successfully")
    })

    // 处理返回文件
    r.GET("/download", func(c *gin.Context) {
        filename := "example.txt"
        filepath := "./downloads/" + filename

        // 打开文件
        file, err := os.Open(filepath)
        if err != nil {
            c.String(http.StatusInternalServerError, "Failed to open file: %s", err.Error())
            return
        }
        defer file.Close()

        // 设置响应头
        c.Header("Content-Disposition", "attachment; filename="+filename)
        c.Header("Content-Type", "application/octet-stream")
        c.Header("Content-Transfer-Encoding", "binary")

        // 将文件内容写入响应主体
        _, err = io.Copy(c.Writer, file)
        if err != nil {
            c.String(http.StatusInternalServerError, "Failed to write file: %s", err.Error())
        }
    })

    r.Run(":8080")
}

路由

设置路由不存在的处理方法:

r.NoRoute(func(c *gin.Context) {
    c.JSON(404, gin.H{"message": "page not found"})
})

限制请求大小:

r.MaxMultipartMemory = 10 << 20 // 10MB
// "github.com/gin-contrib/size"
r.Use(size.Check(size.MB * 10))
r.Use(requestSizeLimitMiddleware(10 << 20))
func requestSizeLimitMiddleware(maxSize int64) gin.HandlerFunc {
    return func(c *gin.Context) {
        if c.Request.ContentLength > maxSize {
            c.AbortWithStatusJSON(413, gin.H{"message": "request entity too large"})
            return
        }
        c.Next()
    }
}