#62 转载:Diss Golang
Golang 2021-09-06作者可能比较喜欢 C# (C# 的特性你让我丢掉哪一个我都觉得少块肉), 对 Golang 进行了一些批评,认为其设计缺乏远见,存在很多缺陷:
Anders Hejlsberg 和 Microsoft 把最佳设计都端到眼前了,其他语言纷纷各取所需,但是 Golang 的设计者却不为所动。
#61 Golang 学习资料
Golang 2021-08-24- 官方文档镜像:https://docs.studygolang.com/
- Golang PlayGround: https://play.golang.org/ (自动跳到 https://go.dev/play/)
- https://awesome-go.com/
- https://github.com/avelino/awesome-go
- https://github.com/jobbole/awesome-go-cn 中文版
- Go语言爱好者周刊
- 开源图书
- 《Go 入门指南》
《The Way to Go》中文版 - 《高效的 Go 编程》
《Effective Go》中文版 - 《Go语言标准库》
《The Golang Standard Library by Example》中文版 - 极客兔兔,七天用Go从零实现系列
#60 Golang 定时任务
Golang 2021-08-23time.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)
}
#59 Gin 源码阅读 [编辑中]
Golang Gin 2021-08-17:) 本文正在编辑中,暂时不提供浏览...
#58 Beego 路由
Golang Beego BeegoNotes 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 方法到指定方法。
- 支持基础路由中提到到八种方法(Any 用星号代替,优先级最低)。
- 如果不指定这个参数,会映射 GET 请求到
Get方法,以此类推。 - 应该不支持指定多个方法。
- 应该不支持重复指定方法。
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") 获取。
注解路由
- 2.0 开始支持,dev 模式生效
- 自动扫描指定目录,生成
routers/commentsRouter.go文件 CommentRouterPath配置扫描目录
web.Include(&CMSController{})
相应的控制器需要添加这样格式的注解:
// @router /staticblock/:key [get]
#57 Beego 框架: 入门
Golang Beego BeegoNotes 2021-07-07照着官网文档 http://beego.vip 过一遍。
PS: 我之前看的网址是 beego.me, 不知道为啥换了域名。
#56 Golang RabbitMQ
Golang RabbitMQ MQ 2021-06-18- 2014/07/11, RabbitMQ & AMQP
- 2015/02/12, Python RabbitMQ
- 2015/04/01, RabbitMQ 管理命令
- 2015/05/04, pika 01: 基础
- 2015/05/04, pika 02: BlockingConnection
- 2015/05/05, pika 03: SelectConnection
- 2015/05/06, pika 04: TornadoConnection
- 2016/02/12, Java RabbitMQ
- 2018/01/31, RabbitMQ 集群
- 2018/04/01, 面试题:RabbitMQ 相关
- 2018/10/02, pika: AsyncioConnection
- 2018/10/06, 使用 RabbitMQ 遇到的一个小问题
- 2019/01/12, 延迟队列
- 2020/10/03, 常见的消息队列
- 2021/06/18, Golang RabbitMQ
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)
生产者
生产者基本流程
生产者:连接
amqp.Dial->amqp.Connectionamqp.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 配好就行,但从稳妥起见,还是在连接时加上比较好。
amqp.Channel.QueueDeclareamqp.Channel.ExchangeDeclareamqp.Channel.QueueBind
q, err := ch.QueueDeclare(
"hello", // 队列名称
true, // 持久化
false, // 自动删除
false, // 独占
false, // 等待服务器回应
nil, // 额外参数
)
if err != nil {
log.Fatalf("Failed to declare a queue: %s", err)
}
生产者:发送
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)
}
生产者:收尾
amqp.Connection.Closeamqp.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
Golang Redis 2021-06-05package 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
Golang DB 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
}