#554 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
然后就是配置阶段,消费者只用关心队列在不在。

参考资料与拓展阅读

#553 UUID

2021-06-17

Universally Unique Identifier 通用唯一识别码
是 ISO/IEC 标准,也定义在 IETF 的 RFC4122 中。
128 位,也就是 16 字节,通常使用 32 位 16 进制数字,以 8-4-4-4-12 的形式表示,例如:d09abf7e-3e39-11ec-9dbc-b1755772e461

#552 NanoID 能不能取代 UUID

2021-06-17

最近听说一种新的 ID 生成器,叫做 NanoID,很多地方那个都把它拿来和 UUID 做对比。
我的结论:NanoID 没啥了不起,就是个随机字符串,取代不了 UUID。

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

#548 采用霍夫变换矫正倾斜图片

2021-06-04

原图(内容成 15° 娇):

原图

原理

里面设计很多知识点,我一个都不知道,涉及图像处理,数学计算,信号变换等多个方面。

但我查到的资料似乎和下面两个概念相关。

如果看到这篇文章的人了解其中原理能够跟我讲讲 (ninedoors#126),万分感激!

傅立叶变换, Fourier Transform, FT

傅里叶变换是一种线性积分变换,用于信号在时域(或空域)和频域之间的变换,在物理学和工程学中有许多应用。

我的理解是用多个三角函数来表示一个变化曲线。

霍夫变换, Hough Transform

霍夫变换是一种特征提取,被广泛应用在图像分析、电脑视觉以及数位影像处理。霍夫变换是用来辨别找出物件中的特征,例如:线条。他的算法流程大致如下,给定一个物件、要辨别的形状的种类,算法会在参数空间中执行投票来决定物体的形状,而这是由累加空间(accumulator space)里的局部最大值来决定。

维基百科说的很清楚了,用来分析图片特征,识别出其中的形状。

步骤

  1. 原图(灰度处理 + 尺寸裁剪),减少无关数据,减少计算量
  2. 傅里叶变换,得到频域图像
  3. 利用霍夫变换做直线检测
  4. 计算倾斜角度
  5. 旋转校正

代码

import logging
import math

import cv2
import numpy as np
from numpy.lib.function_base import average

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s [%(name)s:%(funcName)s#%(lineno)s] %(message)s')
LOG = logging.getLogger(__name__)

# 1、灰度化读取文件
filepath = '/tmp/rotated.png'
img = cv2.imread(filepath, 0)
if img is None:
    LOG.error('image not exists!')
    exit()

# 2、图像延扩
h, w = img.shape[:2]
new_h = cv2.getOptimalDFTSize(h)  # 傅里叶最优尺寸
new_w = cv2.getOptimalDFTSize(w)
right = new_w - w
bottom = new_h - h
# 边界扩充 cv2.copyMakeBorder(src, top, bottom, left, right, borderType, dst=None)
#   BORDER_CONSTANT    常量,增加的变量通通为 value
#   BORDER_REFLICATE   用边界的颜色填充
#   BORDER_REFLECT     镜像
#   BORDER_REFLECT_101 倒映
#   BORDER_WRAP        没有规律
nimg = cv2.copyMakeBorder(img, 0, bottom, 0, right, borderType=cv2.BORDER_CONSTANT, value=0)
cv2.imshow('new image', nimg)

# 3、傅里叶变换,获到频域图像
f = np.fft.fft2(nimg)
fshift = np.fft.fftshift(f)
magnitude = np.log(np.abs(fshift))
LOG.info(magnitude)
# 二值化
magnitude_uint = magnitude.astype(np.uint8)
ret, thresh = cv2.threshold(magnitude_uint, 11, 255, cv2.THRESH_BINARY)
LOG.info(ret)
cv2.imshow('thresh', thresh)
LOG.info(thresh.dtype)

# 4、霍夫直线变换
lines = cv2.HoughLinesP(thresh, 2, np.pi/180, 30, minLineLength=40, maxLineGap=100)
LOG.info('line number: %d', len(lines))
# 创建一个新图像,标注直线
lineimg = np.ones(nimg.shape, dtype=np.uint8)
lineimg = lineimg * 255
for index, line in enumerate(lines):
    LOG.info('draw line#%d: %s', index, line)
    x1, y1, x2, y2 = line[0]
    cv2.line(lineimg, (x1, y1), (x2, y2), (0, 255, 0), 2)
cv2.imshow('line image', lineimg)

# 5、计算倾斜角度
piThresh = np.pi / 180
pi2 = np.pi / 2
angles = []
for line in lines:
    LOG.info('line#%d: %s <===============', index, line)
    x1, y1, x2, y2 = line[0]
    if x2 - x1 == 0:
        LOG.debug('skip 1')
        continue
    theta = (y2 - y1) / (x2 - x1)
    LOG.debug('theta: %r', theta)
    if abs(theta) < piThresh or abs(theta - pi2) < piThresh:
        LOG.debug('skip 2: %r', theta)
        continue
    angle = math.atan(theta)
    LOG.info('angle 1: %r', angle)
    angle = angle * (180 / np.pi)
    LOG.info('angle 2: %r', angle)
    angle = (angle - 90)/(w/h)
    LOG.info('angle 3: %r', angle)
    angles.append(angle)

if not angles:
    LOG.info('图片挺正的了,别折腾!')
else:
    # from numpy.lib.function_base import average
    # angle = average(angles)
    LOG.info('  方差: %r', np.array(angles).var())
    LOG.info('标准差: %r', np.array(angles).std())
    # angle = np.mean(angles)
    import statistics
    # LOG.debug(statistics.multimode(angles))
    # angle = statistics.mode(angles)
    # statistics.StatisticsError: geometric mean requires a non-empty dataset  containing positive numbers
    # statistics.StatisticsError: harmonic mean does not support negative values
    angle = statistics.median(angles)
    if 180 > angle > 90:
        angle = 180 - angle
    elif -180 < angle < -90:
        angle = 180 + angle
    LOG.info('==> %r, %r', angles, angle)

    # 6、旋转
    center = (w//2, h//2)
    M = cv2.getRotationMatrix2D(center, angle, 1.0)
    LOG.debug('=========== RotationMatrix2D ===========')
    for i in M:
        LOG.debug(i)
    LOG.debug('^======================================^')
    rotated = cv2.warpAffine(img, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
    cv2.imshow('rotated', rotated)

cv2.waitKey(0)
cv2.destroyAllWindows()

参考资料与拓展阅读

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

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

#545 Golang JSON

2021-06-02

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

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