#62 转载:Diss Golang

2021-09-06

作者可能比较喜欢 C# (C# 的特性你让我丢掉哪一个我都觉得少块肉), 对 Golang 进行了一些批评,认为其设计缺乏远见,存在很多缺陷:

Anders Hejlsberg 和 Microsoft 把最佳设计都端到眼前了,其他语言纷纷各取所需,但是 Golang 的设计者却不为所动。

#61 Golang 学习资料

2021-08-24
  1. 官方文档镜像:https://docs.studygolang.com/
  2. Golang PlayGround: https://play.golang.org/ (自动跳到 https://go.dev/play/)
  3. https://awesome-go.com/
  4. https://github.com/avelino/awesome-go
  5. https://github.com/jobbole/awesome-go-cn 中文版
  6. Go语言爱好者周刊
  7. 开源图书
  8. Go 入门指南
    《The Way to Go》中文版
  9. 高效的 Go 编程
    《Effective Go》中文版
  10. Go语言标准库
    《The Golang Standard Library by Example》中文版
  11. 极客兔兔,七天用Go从零实现系列

#60 Golang 定时任务

2021-08-23

time.Ticker

异步执行(需要程序其他部分实现阻塞)

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建一个每秒触发一次的定时器
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop() // 程序结束时停止定时器

    // 启动一个 goroutine 执行定时任务
    go func() {
        for _ = range ticker.C {
            // 每次定时器触发时执行任务
            fmt.Printf("ticked at %v\n", time.Now())
        }
    }()

    // 让主线程等待,以便定时任务能够执行
    // 例如,等待 5 分钟后退出
    time.Sleep(5 * time.Minute)
}

同步执行(主线程阻塞)

在主线程内的 for 循环中使用 select 来等待定时器触发。
主线程会一直阻塞在 select 语句中,直到收到定时器的触发信号。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 创建一个每秒触发一次的定时器
    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop() // 程序结束时停止定时器

    // 使用一个无限循环来监听定时器的信号
    for {
        select {
        case <-ticker.C: // 当定时器触发时,执行任务
            fmt.Println("执行定时任务:", time.Now())
        }
    }
}

time.After

package main

import (
    "fmt"
    "time"
)

func main() {
    // 延迟 5 秒后执行任务
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("执行延迟任务:", time.Now())
    }
}

time.Sleep

package main

import (
    "fmt"
    "time"
)

func main() {
    // 每隔 3 秒执行一次任务
    for {
        fmt.Println("执行定时任务:", time.Now())
        time.Sleep(3 * time.Second)
    }
}

time.AfterFunc

package main

import (
    "fmt"
    "time"
)

func main() {
    // 延迟 3 秒执行任务
    time.AfterFunc(3*time.Second, func() {
        fmt.Println("延时执行的任务:", time.Now())
    })

    // 主线程继续运行,避免程序退出
    time.Sleep(5 * time.Second)
}

#58 Beego 路由

2021-08-02

基础路由

web.Get(router, web.HandleFunc)
web.Post(router, web.HandleFunc)
web.Put(router, web.HandleFunc)
web.Patch(router, web.HandleFunc)
web.Head(router, web.HandleFunc)
web.Options(router, web.HandleFunc)
web.Delete(router, web.HandleFunc)
web.Any(router, web.HandleFunc)

控制器路由

// func Router(rootpath string, c ControllerInterface, mappingMethods ...string) *HttpServer {
//  return BeeApp.Router(rootpath, c, mappingMethods...)
// }
beego.Router("/admin", &admin.UserController{})
  • 默认匹配 /:id, /?:id
  • 类型匹配 /:id:int, /:id:string
  • 正则匹配 /:id([0-9]+)
  • 星号匹配 /username/* => :splat 变量
    /username/*.* => :path 变量和 :ext 变量

取变量的方式:

c.Ctx.Input.Param(":id")

mappingMethods

映射 HTTP 方法到指定方法。

  1. 支持基础路由中提到到八种方法(Any 用星号代替,优先级最低)。
  2. 如果不指定这个参数,会映射 GET 请求到 Get 方法,以此类推。
  3. 应该不支持指定多个方法。
  4. 应该不支持重复指定方法。
web.Router("/api/food",&RestController{},"get:ListFood")
web.Router("/api/food",&RestController{},"post:CreateFood")
web.Router("/api/food",&RestController{},"put:UpdateFood")
web.Router("/api/food",&RestController{},"delete:DeleteFood")

web.Router("/api",&RestController{},"get,post:ApiFunc")

web.Router("/api/food",&RestController{},"get:ListFood;post:CreateFood;put:UpdateFood;delete:DeleteFood")

注意:控制器可以声明 URLMapping 方法,比 mapptingMethods 参数通过反射实现更加高效。

func (c *CMSController) URLMapping() {
    c.Mapping("StaticBlock", c.StaticBlock)
    c.Mapping("AllBlock", c.AllBlock)
}

自动路由

web.AutoRouter(&controllers.ObjectController{})

URL 采用 /:controller/:method 前缀的方式,后面的部分会转化成 map 参数 (.Ctx.Input.Params)。
method 不区分大小写,对应的处理方法名首字母大写,比如 login -> Login。

注意:/system/config.json 对应到 SystemController.Config 方法,后缀通过 .Ctx.Input.Param(":ext") 获取。

注解路由

  1. 2.0 开始支持,dev 模式生效
  2. 自动扫描指定目录,生成 routers/commentsRouter.go 文件
  3. CommentRouterPath 配置扫描目录
web.Include(&CMSController{})

相应的控制器需要添加这样格式的注解:

// @router /staticblock/:key [get]

#56 Golang RabbitMQ

2021-06-18

RabbitMQ 是啥就不说了,怎么安装部署也不说了,就记录一下 RabbitMQ 在 Golang 开发中的应用。

说明:采用 github.com/streadway/amqp 库。

func (ch *Channel) Publish(exchange, key string, mandatory, immediate bool, msg Publishing) error
func (ch *Channel) Consume(queue, consumer string, autoAck, exclusive, noLocal, noWait bool, args Table) (<-chan Delivery, error)

生产者

生产者基本流程

生产者:连接

  1. amqp.Dial -> amqp.Connection
  2. amqp.Connection.Channel -> amqp.Channel
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
    log.Fatalf("Failed to connect to RabbitMQ: %s", err)
}
ch, err := conn.Channel()
if err != nil {
    log.Fatalf("Failed to open a channel: %s", err)
}

生产者:配置(可选)

事先把 MQ 配好就行,但从稳妥起见,还是在连接时加上比较好。

  1. amqp.Channel.QueueDeclare
  2. amqp.Channel.ExchangeDeclare
  3. amqp.Channel.QueueBind
q, err := ch.QueueDeclare(
    "hello", // 队列名称
    true,    // 持久化
    false,   // 自动删除
    false,   // 独占
    false,   // 等待服务器回应
    nil,     // 额外参数
)
if err != nil {
    log.Fatalf("Failed to declare a queue: %s", err)
}

生产者:发送

  1. amqp.Channel.Publish
err = ch.Publish(
    "",     // exchange
    q.Name, // routing key
    false,  // mandatory
    false,  // immediate
    amqp.Publishing{
        ContentType: "text/plain",
        Body:        []byte("hello world"),
    })
if err != nil {
    log.Fatalf("Failed to publish a message: %s", err)
}

生产者:收尾

  1. amqp.Connection.Close
  2. amqp.Channel.Close

消费者

消费者基本流程

和生产者基本一致。只是调用的的是 chan.Consume 而不是 chan.Publish
然后就是配置阶段,消费者只用关心队列在不在。

package main

import (
    "context"
    "encoding/json"
    "log"
    "time"

    "github.com/streadway/amqp"
)

// Consumer RabbitMQ 消费者
type Consumer struct {
    conn    *amqp.Connection
    channel *amqp.Channel
    queue   string
}

// NewConsumer 创建消费者
func NewConsumer(conn *amqp.Connection, ch *amqp.Channel, queueName string) (*Consumer, error) {
    // 声明队列
    _, err = DeclareQueue(ch, queueName)
    if err != nil {
        return nil, err
    }

    return &Consumer{
        conn:    conn,
        channel: ch,
        queue:   queueName,
    }, nil
}

// Consume 消费消息
func (c *Consumer) Consume(ctx context.Context, workerID int) error {
    // 设置 QoS
    err := c.channel.Qos(
        1,     // prefetch count
        0,     // prefetch size
        false, // global
    )
    if err != nil {
        return err
    }

    // 开始消费
    msgs, err := c.channel.Consume(
        c.queue,        // queue
        "",             // consumer
        false,          // auto-ack
        false,          // exclusive
        false,          // no-local
        false,          // no-wait
        nil,            // args
    )
    if err != nil {
        return err
    }

    log.Printf("消费者 %d 已启动,等待消息...", workerID)

    for {
        select {
        case <-ctx.Done():
            log.Printf("消费者 %d 停止", workerID)
            return nil
        case msg, ok := <-msgs:
            if !ok {
                log.Printf("消费者 %d 消息通道关闭", workerID)
                return nil
            }

            // 处理消息
            var message Message
            if err := json.Unmarshal(msg.Body, &message); err != nil {
                log.Printf("解析消息失败: %v", err)
                msg.Nack(false, false) // 拒绝消息,不重新入队
                continue
            }

            // 模拟处理时间
            log.Printf("消费者 %d 处理消息: %+v", workerID, message)
            time.Sleep(1 * time.Second)

            // 确认消息
            if err := msg.Ack(false); err != nil {
                log.Printf("确认消息失败: %v", err)
            }
        }
    }
}

// Close 关闭连接
func (c *Consumer) Close() {
    if c.channel != nil {
        c.channel.Close()
    }
    if c.conn != nil {
        c.conn.Close()
    }
}

复习:Exchange Types

  • Fanout: 忽略 routingKey / bindingKey,将消息广播给所有绑定队列
  • Header: 忽略 routingKey / bindingKey,根据消息和队列绑定时指定的 headers 匹配
    • 支持精准匹配、数字匹配
    • 匹配规则:x-match = all(默认)/ any
    • 据说性能很差,没有使用过,官方文档都没有提供代码示例
  • Direct: routingKey = bindingKey
  • Topic: routingKey 模糊匹配 bindingKey(* 表示一个单词,# 表示若干个单词)

默认 Exchange

  • 名称:空字符串
  • 类型:Direct
  • 特性:所有已声明的队列都会隐式绑定(implicit route)到默认 Exchange,bindingKey queue.Name
    所以 routingKey
    quene.Name 就可以将消息发送给对应的队列

参考资料与拓展阅读

#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 查