阅读视图

发现新文章,点击刷新页面。
🔲 ⭐

漫谈 Golang 之 map

map 参数传递

当 map 作为参数被传递时,实际上传递的 map 的指针信息,传递后的修改会同步到函数外。

可以看到 m 被传递到了 onMap 函数中,但是最后在函数外的 m 也是被修改了的。

package mainimport "fmt"func main() {    m := map[int]int{}    opMap(m)    printMap(m)}func opMap(m map[int]int) {    for i := 0; i < 10; i++ {        m[i] = i        printMap(m)    }}func printMap(m map[int]int) {    fmt.Printf("len: %v, map: %v\n", len(m), m)}// Output:// len: 1, map: map[0:0]// len: 2, map: map[0:0 1:1]// len: 3, map: map[0:0 1:1 2:2]// len: 4, map: map[0:0 1:1 2:2 3:3]// len: 5, map: map[0:0 1:1 2:2 3:3 4:4]// len: 6, map: map[0:0 1:1 2:2 3:3 4:4 5:5]// len: 7, map: map[0:0 1:1 2:2 3:3 4:4 5:5 6:6]// len: 8, map: map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7]// len: 9, map: map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8]// len: 10, map: map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9]// len: 10, map: map[0:0 1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9]

无法对 map value 取地址

如下的代码中,尝试获取 map value 的地址,会在编译时显示失败。

Why Go forbid taking the address of map member

package mainimport "fmt"func main() {    m := map[int]int{}    address := &m[0] // invalid operation: cannot take address of m[0] (map index expression of type int)    fmt.Println(address)}
🔲 ⭐

漫谈 Golang 之 slice

Part1: array & slice

// define a slices := make([]int, 10)// define an arraya := [10]int{}
  • 定义 slice 第一种方式
    var s []int
  • 定义 slice 第二种方式
    s := make([]int, len, cap)
  • array
    var a [length]type// var a [10]int

Part2: 类型

package mainimport "fmt"func main() {    var a [8]int    printArray(a)}func printArray(a [10]int) {    fmt.Println(len(a))    fmt.Println(cap(a))}

以上代码中,printArray(a) 是否可以正常执行?答案是否定的,编译期就会提示错误

cannot use a (type [8]int) as type [10]int in argument to printArray

可以看到提示中变量 a 的类型是 type [8]int,而函数 printArray 要求的入参类型是 type [10]int。可见 array 的长度也是其类型的一部分!

Part3: slice grow

package mainimport "fmt"func main() {    var s []int    for i := 0; i < 1025; i++ {        s = append(s, i)    }    fmt.Println(len(s)) // 1025    fmt.Println(cap(s)) // 1280 = 1024*1.25}

slice 扩容的方式是:source code

  • cap < 1024 –> cap * 2
  • cap > 1024 –> cap * 1.25

append 多个参数的对于 slice 容量的影响是不同的,特殊case:

package mainimport "fmt"func main() {    var s1, s2, s3, s4, s5 []int    s1 = append(s1, 0) // len 1, cap 1    printSlice(s1)    s2 = append(s2, 0, 1) // len 2, cap 2    printSlice(s2)    s3 = append(s3, 0, 1, 2) // len 3, cap 3    printSlice(s3)    s4 = append(s4, 0, 1, 2, 3) // len 4, cap 4    printSlice(s4)    s5 = append(s5, 0, 1, 2, 3, 4) // len 5, cap 6    printSlice(s5)}func printSlice(s []int) {    fmt.Println(len(s))    fmt.Println(cap(s))}

Part4: slice append
如何快速完成一次 slice 数据填充?

  1. 声明一个 slice 直接开始 append
  2. 声明固定长度 slice 后开始 append
  3. 声明固定长度 slice 后,使用 index 进行数据填充

可以看到方法三是最快的。

对于大数组的赋值,常用的优化方式还有一种 BCE,可以参考这篇文章中的用法,不再赘述。golang 边界检查优化

package mainimport "testing"func BenchmarkAppend(b *testing.B) {    var s []int    for i := 0; i < b.N; i++ {        s = append(s, i)    }}func BenchmarkMakeSliceAppend(b *testing.B) {    s := make([]int, 0, b.N)    for i := 0; i < b.N; i++ {        s = append(s, i)    }}func BenchmarkIndex(b *testing.B) {    s := make([]int, b.N)    for i := 0; i < b.N; i++ {        s[i] = i    }}// Output// goos: darwin// goarch: amd64// cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz// BenchmarkAppend-8                457261122            14.93 ns/op// BenchmarkMakeSliceAppend-8       1000000000             1.407 ns/op// BenchmarkIndex-8                 1000000000             1.246 ns/op

Part5: slice 扩容带来的”意外”

当使用 index 修改 slice 时,可能会出现”时而生效时而不生效的情况”,究其原因是 slice 在 grow 的过程中重新分配了内存地址。

下面这个情况展示接收 slice 的函数 sliceAppend 修改了 index 但是在函数外不生效的情况。

package mainimport "fmt"func main() {    var s []int    s = append(s, 1)    printSlice(s)    sliceAppend(s)    printSlice(s)}func sliceAppend(s []int) {    s = append(s, 1) // 此处发生了扩容操作,导致 s 的内存地址改变    s[0] = 0    printSlice(s)}func printSlice(s []int) {    fmt.Printf("len: %v, cap: %v, val: %v\n", len(s), cap(s), s)}// Output:// len: 1, cap: 1, val: [1]// len: 2, cap: 2, val: [0 1]// len: 1, cap: 1, val: [1]

其实,扩容的发生导致函数内外的可见性也不一样了。和上个例子差不多的一个案例。
可以看到,此处两个 slice s 打印出来,结果是不一样的。看起来就是函数内的 s 比函数外的 s 数据更多了!换句话说,在实际场景中,很有可能因为如下这样的误操作,导致看似操作过 s,但是数据缺丢失了。

package mainimport "fmt"func main() {    var s []int    s = append(s, 1)    printSlice(s)    sliceAppend(s)    printSlice(s)}func sliceAppend(s []int) {    s = append(s, 1) // 此处发生了扩容操作,导致 s 的内存地址改变    printSlice(s)}func printSlice(s []int) {    fmt.Printf("len: %v, cap: %v, val: %v\n", len(s), cap(s), s)}// Output:// len: 1, cap: 1, val: [1]// len: 2, cap: 2, val: [1 1]// len: 1, cap: 1, val: [1]

!!如果发生了扩容,修改会在新的内存中!!

所以针对 slice 的操作,务必使用 append 函数返回的 slice 对象进行后续操作,避免出现奇怪的数据异常!

Part6: slice 序列化

如下案例所示,slice 的 zero value 经过默认的 json 库序列化结果是 null,但是初始化的 slice 经过默认的 json 库序列化结果就是 []

package mainimport (    "encoding/json"    "fmt")func main() {    var s []int    b, _ := json.Marshal(s)    fmt.Println(string(b))}// Output:// null
package mainimport (    "encoding/json"    "fmt")func main() {    s := []int{}    b, _ := json.Marshal(s)    fmt.Println(string(b))}// Output:// []
package mainimport (    "encoding/json"    "fmt")func main() {    s := make([]int, 0, 1)    b, _ := json.Marshal(s)    fmt.Println(string(b))}// Output:// []
package mainimport (    "encoding/json"    "fmt")func main() {    s := make([]int, 1)    b, _ := json.Marshal(s)    fmt.Println(string(b))}// Output:// [0]

TODO 如果有更多再补充

🔲 ⭐

Golang 开源之 tparse 使用指南

tparse 分析和归纳 go test 输出的命令行工具

安装

go install github.com/mfridman/tparse@latest

tparse 能做什么

仅使用 go test -v 来显示测试结果,只是罗列了所有测试集合,没有归纳总结,不便于一眼明了。

➜  go test -race ./testtparse -v=== RUN   Test_sum=== RUN   Test_sum/return_2--- PASS: Test_sum (0.00s)    --- PASS: Test_sum/return_2 (0.00s)=== RUN   Test_sub=== RUN   Test_sub/return_0--- PASS: Test_sub (0.00s)    --- PASS: Test_sub/return_0 (0.00s)PASSok      code.byted.org/demo/testtparse  0.026s

使用 tparse 分析归纳 go test 的报告结论

➜  go test -race ./testtparse -json -cover | tparse -all+--------+---------+-------------------+------------+| STATUS | ELAPSED |       TEST        |  PACKAGE   |+--------+---------+-------------------+------------+| PASS   |    0.00 | Test_sum          | testtparse || PASS   |    0.00 | Test_sum/return_2 | testtparse || PASS   |    0.00 | Test_sub          | testtparse || PASS   |    0.00 | Test_sub/return_0 | testtparse |+--------+---------+-------------------+------------++--------+---------+--------------------------------+--------+------+------+------+| STATUS | ELAPSED |            PACKAGE             | COVER  | PASS | FAIL | SKIP |+--------+---------+--------------------------------+--------+------+------+------+| PASS   | 0.04s   | code.byted.org/demo/testtparse | 100.0% |    4 |    0 |    0 |+--------+---------+--------------------------------+--------+------+------+------+
🔲 ⭐

Golang 开源之 semgroup 使用指南

semgroup 用于并发执行一组任务,提供限制协程数量、同步等待任务执行完成及错误信息传递的能力。不同于 errgroup ,semgroup 会执行所有任务,且收集任务产生的 error 信息。在全部任务执行完成后,将收集的 error 信息返回给开发者。

使用方法

快速使用请看官网的使用方法,此处不赘述

示例 TestDemo 中用到了如下功能,可以按需使用

  1. context 定义了超时时间,可以限制整个任务的执行时间

  2. 任务并行执行,且向外丢出了自定义 error

  3. 使用 errors.Iserrors.As 处理 error

  4. 使用了拓展的 ExportMultiError 函数将所有内部错误全部输出(附带错误中包含了关键信息如 taskId,可以在后续的程序中使用)

    func TestDemo(t *testing.T) { // 可控制任务整体的执行时间 timedCtx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() g := NewGroup(timedCtx, 1) fe := fooErr{taskId: 1, msg: "__foo__"} g.Go(func() error { return fe }) g.Go(func() error { return os.ErrClosed }) g.Go(func() error { return nil }) err := g.Wait() if err == nil {     t.Fatalf("g.Wait() should return an error") } var (     fbe fooErr ) // Is 可用于抛出错误的判断 if !errors.Is(err, fe) {     t.Errorf("error should be equal fooErr") } // Is 可用于抛出错误的判断 if !errors.Is(err, os.ErrClosed) {     t.Errorf("error should be equal os.ErrClosed") } // As 可以将错误检索出来 if !errors.As(err, &fbe) {     t.Error("error should be matched foobarErr") } // 通过上面 As 将错误信息取出,应该可以拿到失败的任务 id if fbe.taskId != 1 {     t.Error("fooErr task id should be 1") } me, isMultiError := ExportMultiError(err) if !isMultiError {     t.Error("err should be a multiError") } for _, e := range me {     t.Logf("range me: %v", e)     var fe fooErr     if errors.As(e, &fe) && fe.taskId != 1 {         t.Error("variable t.taskId should be 1")     } }}

补充上文使用到的 ExportMultiError 函数。

func ExportMultiError(err error) ([]error, bool) {    if err == nil {        return nil, false    }    switch err.(type) {    case multiError:        return err.(multiError), true    default:        return []error{err}, false    }}

思考

并行处理失败的任务有无必要返回具体的信息?原库中仅透出了 errors.Iserrors.As 两个函数供处理异常,实际上会不会在 error 中透出具体的任务信息,供错误失败时使用呢?
基于这种思考,先提供了一个 ExportmultiError 的函数解决这个问题。有无更好的方式,或者业界更通用的处理方案?

🔲 ⭐

Go 设计模式之 Options-Pattern

Options Pattern 主要是使用于装配属性,让我们先来看看传统的属性装配方案。

结构体

type House struct {    Material     string    HasFireplace bool    Floors       int}

通过构造函数(Constructor)装配属性

// NewHouse("concrete", 5, true)func NewHouse(m string, f int, fp bool) *House {    return &House {        Material: m,        HasFireplace: fp,        Floors: f,    }}

可以看到此时通过一个自定义的构造函数装配属性,此时需装配的属性需要一次性全部填入,且构造函数的入参有顺序性,必须按照函数定义的顺序传入参数。另外,当需装配的属性过于多时,此时构造函数也会越来越冗长。

通过 func 作为参数传入构造函数

type HouseOption func(*House)func WithConcrete() HouseOption {    return func(h *House) {        h.Material = "concrete"    }}func WithoutFireplace() HouseOption {    return func(h *House) {        h.HasFireplace = false    }}func WithFloors(floors int) HouseOption {    return func(h *House) {        h.Floors = floors    }}func NewHouse(opts ...HouseOption) *House {    const (        defaultFloors       = 2        defaultHasFireplace = true        defaultMaterial     = "wood"    )    h := &House{        Material:     defaultMaterial,        HasFireplace: defaultHasFireplace,        Floors:       defaultFloors,    }    // Loop through each option    for _, opt := range opts {        // Call the option giving the instantiated        // *House as the argument        opt(h)    }    // return the modified house instance    return h}// build House with optionsh := NewHouse(  WithConcrete(),  WithoutFireplace(),  WithFloors(3),)

将 func 作为参数传入,一是方便了装配属性的复杂配置,二是不需要固定顺序的构造参数传入,三其实这样的实现方式也可以作为一个属性装配的切面,可以暗搓搓整点活儿。

这就是 Options Patter 了。

参考资料:

🔲 ⭐

git 使用手册

[TOC]

git log 使用

git rebase

---A---B---C---D(master)        \         \---E'---F' (feat)j

当前开发在 feat 分支,需要合并 master 代码,使用:

➜ git rebase -i master

合并的时候处理好冲突,合并结束后:

            (master)---A---B---C---D---E'---F'(feat)

此时如果需要将 master 更新到 feat 处,可以使用:

➜ git checkout master➜ git merge --ff feat

执行完成后:

---A---B---C---D---E'---F'(feat)                         \                          \(master)

删除 feat 分支可以使用:

➜ git branch -d feat

此时分支链路为:

---A---B---C---D---E---F(master)

精简 log 打印

git log --online# 01e21e1 (HEAD -> master, origin/master, origin/HEAD) !2 Feature:add lambda support# 55118f9 Feature:add lambda support# 96c6a84 !1 Feat:clean code fragement Merge pull request !1 from 寒沧丶/clean# d52d730 clean:clean code fragment# 0b2ab33 update .gitignore.# d682d2b update README.md.

查询时间范围内的 log 信息

# 只显示2020-08-01到2020-08-08日的提交 git log --after="2020-08-01" --before="2020-08-08"# 显示昨天之后的提交git log --after="yesterday"# 显示这一星期的提交git log --after="1 week ago"git log --after="10 day ago"git log --after="1 month ago"

在log 中展示变更

git log -p# commit 679890d2e3cd7cb53ca586cea72ad1d5abb472e5# Author: tianyaleixiaowu <272551766@qq.com># Date:   Mon May 11 21:55:34 2020 +0800# #     update QuickStart.md.# # diff --git a/QuickStart.md b/QuickStart.md# index 01c8cc9..0bbc812 100644# --- a/QuickStart.md# +++ b/QuickStart.md# @@ -26,7 +26,7 @@#          <dependency>#             <groupId>com.gitee.jd-platform-opensource</groupId>#             <artifactId>asyncTool</artifactId># -           <version>V1.2-SNAPSHOT</version># +           <version>V1.3-SNAPSHOT</version>^M#         </dependency>

*根据提交者过滤 log *

git log --author="nickChen"

根据提交信息检索 log

git log --grep="README"# -i 忽略大小写git log -i --grep="README"# 正则搜索包含 README 或 changelog 的提交信息git log -i --grep="README\|changelog"

查看某个文件的变更log

# 查看这几个文件的提交记录,并打印diffgit log -p README.md changelog# 上述情况下,查询包含 fix 信息的提交记录git log -i --grep="fix" -p README.md changelog

查看文件内容的变更 log

# 查看提交变更中包含 void begin(); 所有 commit log,并打印 log 变更内容# 根据文件内容的提交来查询变更,比较方便定位代码中的一些变化git log -i -S"void begin();" -p

查看 merge commit log

git log --merges

查看两个分支之间的diff

# 查看在 develop 基础上相较于 master 多了哪些 commitgit log master..develop

自定义 log 的输出格式

git log --pretty=format:"%Cred%an - %ar%n %Cblue %h -%Cgreen %s %n"

具体的格式化方式可以参看文档

🔲 ⭐

Redis 实现 (待补全)

Redis 设计与实现

Redis 的 String 设计

redis 对于 String 类型有自己的实现, 这个实现简称 SDS (simple dynamic string).

SDS 的优势:
- 安全性、效率、功能方面的需求
O(1)的获取字符串长度的效率,根据len字段
杜绝缓冲区溢出、根据free字段判断是否扩容

- 内存预分配:        len小于1M,修改之后的len的长度 = free长度        大于1M,直接free 1M- 惰性释放:        不返还 free 的区域= = 以便下次使用- 二进制安全:        \0 作为分隔符时,因为有len判断字符串长度,不会出现问题    兼容部分C字符串函数

Redis 的链表设计

链表:用于列表键、pub/sub、慢查询、监视器
- 双端:获取前后节点复杂度都O(1)

- 无环:前后NULL节点- 带head tail- 表长len- 多态 void* 保存节点值

Redis 的字典实现

字典:SET 和 HSET
- 使用哈希表错位底层实现,是redis 是数据库的存储方式
- 使用dictht 作为哈希表实现,封装成 dict,内有长度为2的dictht数组,
用来做扩容,标记位为-1时表示不在rehash
dict {
dictht[2]
dictType 实现多态的函数,复制对比删除等
rehashindex -1表示不在rehash

扩展 ht[1]大小为 第一次大于ht[0].used*2 的 2^n
收缩 。。。。。。。。。大于ht[0].used 的 2^n
- 渐进式哈希:每次插入删除查找更新都rehash一次

Redis 的跳表实现

跳表:
- 实现有序集数据
- 随机化数据结构、它的查找添加删除都可以在对数期望时间下完成

Redis 的压缩列表实现

压缩列表:
- ziplist结构:
header - entries - end
bytes-tail-len
- entry结构:
prelen - encoding - len - content

Redis 的引用对象实现

redisObject 结构:
- key 对应的是一个 redisObject 结构,用来多态操作
redisObject {
type.
encoding. type 和 encoding 定位底层数据结构
*ptr
}
引用计数回收

  • 哈希表默认由压缩列表实现:当某个key/value长度大于64

                      或者 entries 的个数大于 512                   会转变成字典实现
  • 列表默认也由压缩列表实现:同上;会转变为双端链表

  • 阻塞:维持一个server[i]->block_keys列表,key为造成阻塞的key,value为客户端链接
    readyList 用来保存即将离开block队列

  • 事务:WATCH MULTI DISCARD EXEC

  • 慢日志查询:设置时间和保存数量

       SLOW GET 查看
❌