Redis基础数据类型和常用操作命令

简述

经常用的都是通过代码包装好的redis调用程式,时间有些常用命令又模糊了,故拿出来记录下。

基础数据类型

String

Add|Set

  • SET key value [EX seconds|PX milliseconds] [NX|XX] NX:相当于SETNX值不存在时增加,XX值存在覆盖
  • MSET key value key value ... 一次设置多个值
  • SETNX key value 原子性操作,当key不存在设置成功返回1,否则返回0
  • STRLEN key 返回key的长度,不存在返回0,非string类型返回错误
  • APPEND key value 在key的末尾追加字符串,返回字符串长度,原来的key不存在,则会增加(相当于SET)
  • SETRANGE key offset value 替换字符串,从offset下标开始,替换strlen(value)位字符串

Get

  • EXISTS key ... 判断key是否存在,返回存在key的个数,单个key不存在返回0
  • GET key 获取key值
  • MGET key ... 获取多个key的值

Delete

  • DEL key ... 删除指定key,成功返回删除个数

string类型在redis中有三种存储方式

  • int 64位有符号位整数
  • embstr 长度小于44个字节的字符串
  • raw 长度大于44个字节的字符串
    其中raw效率相对最差,可以通过 OBJECT ENCODING key的命令来查看string的存储类型

List

Add|Set

  • LPUSH key value ... 左侧写入返回总列表个数
  • RPUSH key value ... 右侧写入返回总列表个数
  • LINSERT key [BEFORE|AFTER] item value 从左侧开始,指定元素(item)前或后插入新元素,返回总列表个数
  • LSET key index 覆盖或增加指定索引的值,返回OK,超过最大list范围则会报错

Get

  • LRANGE key start stop 获取指定范围的list, lrange key 0 -1 标识获取整个list
  • LINDEX key index 获取指定索引处的元素,不存在返回nil
  • LLEN key 获取列表长度

Delete

  • LPOP key 左侧弹出,返回总列表个数,当list不存在时返回nil
  • RPOP key 右侧弹出,返回总列表个数,当list不存在时返回nil
  • LTRIM key start stop 删除指定区间外的元素,返回OK
  • BLPOP key LPOP阻塞式方法
  • BRPOP key RPOP阻塞式方法

Hash

类似于Go中的mapPHP中的键值数组,用于存储字段的映射关系

Add|Set

  • HMSET key field value ... 成功返回 OK
  • HSET key field value ... 增加field时返回新增field值的个数,覆盖时返回0(覆盖成功也是0)
  • HSETNX key field value 当field不存在时增加成功返回1,如果field存则返回0,不可同时进行多个field操作

Get

  • HKEYS key 返回当前hash中所有的field
  • HEXISTS key field 判断当前key中是否存在field,存在返回1,否则返回0
  • HGETALL key 返回当前Key的所有值,当HASH巨大的时候不适合使用此函数
  • HMGET key field ... 返回当前key指定的field值
  • HSCAN key cursor [MATCH field] [COUNT number] 通过指针移动来获取MATCH匹配的值,COUNT代表的是返回的元素个数,但即使设置了count也并不一定代表就返回count个元素,因此不可作为分页式数据
  • HLEN key 获取Hash长度

Delete

  • HDEL key field ... 删除一个或多个field,当key全部删除后,key会被自动清除

Set

Add|Set

  • SADD key member ... 添加集合元素,返回添加的的元素数量

Get

  • SISMEMBER key member 判断是否存在于集合,0表示不存在1存在
  • SMEMBERS key 返回集合所有元素
  • SSCAN key cursor [MATCH member] [COUNT number] 通过指针移动来获取MATCH匹配的值,COUNT代表的是返回的元素个数,但即使设置了count也并不一定代表就返回count个元素,因此不可作为分页式数据
  • SRANDMEMBER key count 随机返回指定count元素
  • SUNION key key ... 集合并集,返回合并后的值
  • SUNIONSTORE storeKey key key ... 并集,并存储至storeKey中,索引相同则覆盖,返回合并个数
  • SINTER 集合交集,返回合交集值
  • SINTERSTORE storeKey key key ... 交集,并存储至storeKey中
  • SDIFF 集合差集,返回合差集值
  • SDIFFSTORE diffKey key key ... 差集,并存储至diffKey中
  • SCARD key 获取集合元素数量

Delete

  • DEL key 删除集合

Golang 锁的简单使用

简述

Golang中的锁机制主要包含互斥锁和读写锁

互斥锁

互斥锁是传统并发程序对共享资源进行控制访问的主要手段。在Go中主要使用
sync.Mutex的结构体表示。

一个简单的示例:

1
2
3
4
5
6
func mutex()  {
var mu sync.Mutex
mu.Lock()
fmt.Println("locked")
mu.Unlock()
}

或者也可以使用defer来实现,这在整个函数流程中全部要加锁时特别有用,还有一个好处就是可以防止忘记Unlock

1
2
3
4
5
6
func mutex()  {
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
fmt.Println("locked")
}

互斥锁是开箱即用的,只需要申明sync.Mutex即可直接使用

1
var mu sync.Mutex

互斥锁应该是成对出现,在同步语句不可以再对锁加锁,看下面的示例:

1
2
3
4
5
6
7
8
9
func mutex()  {
var mu sync.Mutex
mu.Lock()
fmt.Println("parent locked")
mu.Lock()
fmt.Println("sub locked")
mu.Unlock()
mu.Unlock()
}

此时则会出现fatal error: all goroutines are asleep - deadlock!错误

同样,如果多次对一个锁解锁,则会出现fatal error: sync: unlock of unlocked mutex错误

1
2
3
4
5
6
7
func mutex()  {
var mu sync.Mutex
mu.Lock()
fmt.Println("locked")
mu.Unlock()
mu.Unlock()
}

那么在goroutine中是否对外部锁加锁呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func mutex()  {
var mu sync.Mutex
fmt.Println("parent lock start")
mu.Lock()
fmt.Println("parent locked")
for i := 0; i <= 2; i++ {
go func(i int) {
fmt.Printf("sub(%d) lock start\n", i)
mu.Lock()
fmt.Printf("sub(%d) locked\n", i)
time.Sleep(time.Microsecond * 30)
mu.Unlock()
fmt.Printf("sub(%d) unlock\n", i)
}(i)
}
time.Sleep(time.Second * 2)
mu.Unlock()
fmt.Println("parent unlock")
time.Sleep(time.Second * 2)
}

先看上面的函数执行结果

1
2
3
4
5
6
7
8
9
10
11
12
parent lock start
parent locked
sub(0) lock start
sub(2) lock start
sub(1) lock start
parent unlock // 必须等到父级先解锁,后面则会阻塞
sub(0) locked // 解锁后子goroutine才能执行锁定
sub(0) unlock
sub(2) locked
sub(2) unlock
sub(1) locked
sub(1) unlock

为了方便调试,使用了time.Sleep()来延迟保证goroutine的执行
从结果中可以看出,当所有的goroutine遇到Lock时都会阻塞,而当main函数中的Unlock执行后,会有一个优先(无序)的goroutine来占得锁,其它的则再次进入阻塞状态。

总结:

  • 互斥锁必须成对出现
  • 同级别互斥锁不能嵌套使用
  • 父级中如果存在锁,当在goroutine中执行重复锁定操作时goroutine将被阻塞,直到原互斥锁解锁,多个goroutine将会争抢当前锁资源,其它继续阻塞。

读写锁

读写锁和互斥锁不同之处在于,可以分别针对读操作和写操作进行分别锁定,这样对于性能有一定的提升。
读写锁,对于多个写操作,以及写操作和读操作之前都是互斥的这一点基本等同于互斥锁。
但是对于同时多个读操作之前却非互斥关系,这也是相读写锁性能高于互斥锁的主要原因。

读写锁也是开箱即用型的

1
var rwm = sync.RWMutex

读写锁分为写锁和读锁:

  • 写锁定和写解锁

    1
    2
    rwm.Lock()
    rwm.Unlock()
  • 读锁定和读解锁

    1
    2
    rwm.RLock()
    rwm.RUnlock()

读写锁的读锁和写锁不能交叉相互解锁,否则会发生panic,如:

1
2
3
4
5
6
7
func rwMutex()  {
var rwm sync.RWMutex

rwm.Lock()
fmt.Println("locked")
rwm.RUnlock()
}

fatal error: sync: RUnlock of unlocked RWMutex

对于读写锁,同一资源可以同时有多个读锁定,如:

1
2
3
4
5
6
7
8
9
10
11
func rwMutex()  {
var rwm sync.RWMutex

rwm.RLock()
rwm.RLock()
rwm.RLock()
fmt.Println("locked")
rwm.RUnlock()
rwm.RUnlock()
rwm.RUnlock()
}

但对于写锁定只能有一个(和互斥锁相同),同时使用多个会产生deadlockpanic,如:

1
2
3
4
5
6
7
8
9
10
11
func rwMutex()  {
var rwm sync.RWMutex

rwm.Lock()
rwm.Lock()
rwm.Lock()
fmt.Println("locked")
rwm.Unlock()
rwm.Unlock()
rwm.Unlock()
}

goroutine中,写解锁会试图唤醒所有想要进行读锁定而被阻塞的goroutine

而读解锁会在已无任何读锁定的情况下,试图唤醒一个想进行写锁定而被阻塞的goroutine

下面看一个完整示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func rwMutex() {
var rwm sync.RWMutex

for i := 0; i <= 2; i++ {
go func(i int) {
fmt.Printf("go(%d) start lock\n", i)
rwm.RLock()
fmt.Printf("go(%d) locked\n", i)
time.Sleep(time.Second * 2)
rwm.RUnlock()
fmt.Printf("go(%d) unlock\n", i)
}(i)
}
// 先sleep一小会,保证for的goroutine都会执行
time.Sleep(time.Microsecond * 100)
fmt.Println("main start lock")
// 当子进程都执行时,且子进程所有的资源都已经Unlock了
// 父进程才会执行
rwm.Lock()
fmt.Println("main locked")
time.Sleep(time.Second)
rwm.Unlock()
}
1
2
3
4
5
6
7
8
9
10
11
go(0) start lock
go(0) locked
go(1) start lock
go(1) locked
go(2) start lock
go(2) locked
main start lock
go(2) unlock
go(0) unlock
go(1) unlock
main locked

反复执行上述示例中,可以看到,写锁定会阻塞goroutine
最开始先在mainsleep 100ms ,保证子的goroutine会全部执行,而每个子goroutinesleep 2s
此时会阻塞整个main进程,当所有子goroutine执行结束,读解锁后,main的写锁定才会执行。

再看一个读锁定示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func rwMutex5() {
var rwm sync.RWMutex

for i := 0; i <= 2; i++ {
go func(i int) {
fmt.Printf("go(%d) start lock\n", i)
rwm.RLock()
fmt.Printf("go(%d) locked\n", i)
time.Sleep(time.Second * 2)
rwm.RUnlock()
fmt.Printf("go(%d) unlock\n", i)
}(i)
}

fmt.Println("main start lock")
rwm.RLock()
fmt.Println("main locked")
time.Sleep(time.Second * 10)
}
1
2
3
4
5
6
7
8
9
10
11
main start lock
main locked
go(1) start lock
go(1) locked
go(2) start lock
go(2) locked
go(0) start lock
go(0) locked
go(0) unlock
go(1) unlock
go(2) unlock

可以看到读锁定却并不会阻塞goroutine

总结:

  • 读锁定和写锁定对于写操作都是互斥的
  • 读锁定支持多级嵌套,但写锁定无法嵌套执行
  • 如果有写锁定,当多个读解锁全部执行完成后,则会唤起执行写锁定
  • 写锁定会阻塞goroutine(在Lock()时和互斥锁一样,RLock()时先也是等到RUnlock()先执行,才有锁定机会)

单向链表优化

说明

Golang 单向链表 中是通过不断修改next来实现链表
本章通过一个虚拟head方法来优化链表,其原理就是,创建时直接创建headnext,head第一个值始终是nil
通过headnext来作为初始值,并且对原有链表进行相关优化

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
type node struct {
value interface{}
next *node
}

type LinkedList struct {
dummyHead *node
length int
}

func newNode(value interface{}, next *node) *node {
return &node{
value: value,
next: next,
}
}

func NewLinkedList() *LinkedList {
return &LinkedList{
// 一个虚拟的head value,只使用next
dummyHead: newNode(nil, newNode(nil, nil)),
length: 0,
}
}

// 添加元素
func (l *LinkedList) Add(index int, value interface{}) {
l.checkIndexRange(index)

//移动指针到指定下标
prev := l.dummyHead
for i := 0; i < index; i++ {
// 不断的移动链表指针
// dummyHead 有个虚拟value,其实是从dummyHead的next作为第一个值
prev = prev.next
}

// 每当数据更改时,自动修改其head,移动head头指针(往前增加),而不是直接修改next(不是往后增加)
prev.next = newNode(value, prev.next)
l.length ++
}

// 删除元素
func (l *LinkedList) Delete(index int) interface{} {
l.checkIndexRange(index)

prev := l.dummyHead
var current *node
for i := 0; i <= index; i++ {
// 保存当前值
// dummyHead 有个虚拟value,其实是从dummyHead的next作为第一个值
current = prev.next

// 需要删除的值
if i == index {
if current.next != nil {
// 移除指定索引,移动指针
// 当前值的next就等于当前的next,这样就移除了current
prev.next = current.next
} else {
prev.next = nil
}
} else {
prev = current
}
}

l.length --

return current.value
}

// 通过指定索引获取值
func (l *LinkedList) Index(index int) interface{} {
l.checkIndexRange(index)

prev := l.dummyHead
for i := 0; i <= index; i++ {
prev = prev.next
}

return prev.value
}

// 在链表开头插入值
func (l *LinkedList) Unshift(value interface{}) {
l.Add(0, value)
}

// 在链表结尾插入值
func (l *LinkedList) Push(value interface{}) {
l.Add(l.length, value)
}

// 弹出链表最后一个值
func (l *LinkedList) Pop() interface{} {
return l.Delete(l.length - 1)
}

// 弹出链表第一个值
func (l *LinkedList) Shift() interface{} {
return l.Delete(0)
}

//
func (l *LinkedList) Length() int {
return l.length
}

func (l *LinkedList) checkIndexRange(index int) {
if index < 0 || index > l.length {
panic(`下标越界`)
}
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func TestNewLinkedList(t *testing.T) {
list := NewLinkedList()
list.Add(0,"a")
list.Push("b")
list.Add(2,"c")
list.Add(3,"d")

assert.Equal(t,"a",list.Index(0).(string))
assert.Equal(t,"b",list.Index(1).(string))
assert.Equal(t,"c",list.Index(2).(string))
assert.Equal(t,"d",list.Index(3).(string))

// 中间追加
list.Add(2,"e")

assert.Equal(t,"e",list.Index(2).(string))
assert.Equal(t,"c",list.Index(3).(string))
assert.Equal(t,"d",list.Index(4).(string))
assert.Equal(t,5,list.Length())

assert.Equal(t,"a",list.Index(0).(string))
assert.Equal(t,"b",list.Index(1).(string))
assert.Equal(t,"e",list.Index(2).(string))
assert.Equal(t,"c",list.Index(3).(string))
assert.Equal(t,"d",list.Index(4).(string))


assert.Equal(t,"e",list.Delete(2))
assert.Equal(t,4,list.Length())

assert.Equal(t,"a",list.Index(0).(string))
assert.Equal(t,"b",list.Index(1).(string))
assert.Equal(t,"c",list.Index(2).(string))
assert.Equal(t,"d",list.Index(3).(string))

assert.Equal(t,"d",list.Pop())
assert.Equal(t,3,list.Length())
assert.Equal(t,"a",list.Index(0).(string))
assert.Equal(t,"b",list.Index(1).(string))
assert.Equal(t,"c",list.Index(2).(string))

assert.Equal(t,"a",list.Shift())
assert.Equal(t,2,list.Length())
assert.Equal(t,"b",list.Index(0).(string))
assert.Equal(t,"c",list.Index(1).(string))
}

Golang container/list 实现 简单stack

基础实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import "container/list"

type Stack struct {
list *list.List
}

func NewStack() *Stack {
return &Stack{
list: list.New(),
}
}

func (s *Stack) Push(value interface{}) {
s.list.PushBack(value)
}

func (s *Stack) Pop() interface{} {
e := s.list.Back()
if e != nil {
s.list.Remove(e)
return e.Value
}

return nil
}

func (s *Stack) Length() int {
return s.list.Len()
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func TestNewStack(t *testing.T) {
stack := NewStack()
stack.Push("a")
stack.Push("b")
stack.Push("c")
stack.Push("d")
assert.Equal(t,4,stack.Length())
v := stack.Pop()
assert.Equal(t,"d",v.(string))
assert.Equal(t,3,stack.Length())
v = stack.Pop()
assert.Equal(t,"c",v.(string))
assert.Equal(t,2,stack.Length())
v = stack.Pop()
assert.Equal(t,"b",v.(string))
assert.Equal(t,1,stack.Length())
v = stack.Pop()
assert.Equal(t,"a",v.(string))
assert.Equal(t,0,stack.Length())
}

Leetcode刷题之有效的括号#20

题目描述

给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串,判断字符串是否有效。

有效字符串需满足:

左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。

解题思路

由’(‘,’)’,’{‘,’}’,’[‘,’]’等一一对应,想到的应该是数据栈。

左侧结构的符号入栈,遇到右侧的出栈对比,这样即可一一对应。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func IsValid(s string) bool {
stack := make([]string, 0)
for _,v := range strings.Split(s,``) {
if v == `{` || v == `[` || v == `(` {
stack = append(stack, v)
} else {
var sv string
if len(stack) == 0 {
return false
} else if len(stack) == 1 {
sv = stack[0]
stack = stack[0:0]
} else {
sv = stack[len(stack)-1]
stack = stack[:len(stack)-1]
}

if sv == `{` && v != `}`{
return false
} else if sv == `[` && v != `]` {
return false
} else if sv == `(` && v != `)`{
return false
}
}
}

return len(stack) == 0
}

测试

1
2
3
4
5
6
func TestIsValid(t *testing.T) {
assert.Equal(t,false,IsValid("{}{{{{]}}"))
assert.Equal(t,true,IsValid("{}()[]"))
assert.Equal(t,true,IsValid("[{{{}}}]"))
assert.Equal(t,true,IsValid("{([])}"))
}

Golang 单向链表

基础链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

// 一个简单的单向链表
type Node struct {
value string
// 下一个节点
next *Node
}

type List struct {
// 表头
header *Node
// 总长度
length int
}

func NewList() *List {
return &List{
header: nil,
length: 0,
}
}

func newNode(value string, next *Node) *Node {
return &Node{
value: value,
next: next,
}
}

// 添加一个数据至链表
func (l *List) Add(value string) {
if l.length == 0 {
l.header = newNode(value, nil)
l.length = 1
} else {
current := l.header
for {
if current.next == nil {
current.next = newNode(value, nil)
l.length += 1
break
}
current = current.next
}
}
}

// 删除链接数据
func (l *List) Delete(value string) bool {
currentNode := l.header
prevNode := l.header
for {
if currentNode == nil {
return false
}

if currentNode.value == value {
// 如果是头节点
if currentNode == prevNode {
l.header = currentNode.next
} else {
// 否则,上一个节点的next指针等于当前删除的元素的next指针
prevNode.next = currentNode.next
}

l.length -= 1
return true
}

if currentNode.next == nil {
return false
}

prevNode = currentNode
currentNode = currentNode.next

}
}

// 当前链表是否包指定值
func (l *List) Contains(value string) bool {
currentNode := l.header
for {
if currentNode == nil {
return false
}

if currentNode.value == value {
return true
}

currentNode = currentNode.next
}
}

// 当前链表长度
func (l *List) Length() int {
return l.length
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func TestList(t *testing.T) {
list := NewList()
list.Add("a")
list.Add("b")
list.Add("c")
list.Add("d")
assert.Equal(t, 4, list.Length())
assert.Equal(t, true, list.Delete("c"))
//fmt.Println(list.header.next.next)
assert.Equal(t, 3, list.Length())
assert.Equal(t,false,list.Contains("c"))
assert.Equal(t,true,list.Contains("d"))
assert.Equal(t,true,list.Contains("a"))

assert.Equal(t,true,list.Delete("a"))
assert.Equal(t, 2, list.Length())
assert.Equal(t,false,list.Contains("a"))

assert.Equal(t,true,list.Delete("d"))
assert.Equal(t, 1, list.Length())
assert.Equal(t,false,list.Contains("d"))

assert.Equal(t,1,list.Length())
assert.Equal(t,"b",list.header.value)
assert.Nil(t,list.header.next)
}

Golang 二分法查找

递归二分法查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
func BinarySearch(array []int, target int, params ...int) int {
length := len(array)
maxPoint := length-1
var point int
if len(params) == 0 {
point = int(maxPoint / 2)
} else {
point = params[0]
}

if (point == 0 || point == maxPoint) && target != array[point]{
return -1
}

if target == array[point] {
return point
} else if target > array[point] {
max := point + int(math.Floor(float64(point)/2))
if max > maxPoint {
max = maxPoint
}
return BinarySearch(array, target, max)
} else { //target < array[point]
min := point - int(math.Ceil(float64(point)/2))
if min < 0 {
min = 0
}
return BinarySearch(array, target, min)
}
}

// testing
func TestBinarySearch(t *testing.T) {
array := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
assert.Equal(t,11,BinarySearch(array, 12))
assert.Equal(t,-1,BinarySearch(array, 15))
assert.Equal(t,0,BinarySearch(array, 1))
assert.Equal(t,-1,BinarySearch(array, -5))
assert.Equal(t,-1,BinarySearch(array, 15))
assert.Equal(t,10,BinarySearch(array, 11))
assert.Equal(t,4,BinarySearch(array, 5))
}

二分法的提前是有序的排序

通过git subtree来创建自己的子仓库

简述

查看laravel/framework,就会发现其实laravel主包是由多个子包构建。
每个子包又都可以独立使用,主包中相关子包的commit必须一致,同时还需要同步更新方便。如何做到这一点的呢?

Subtree

subtree 是什么

git subtree 可以实现一个仓库作为其他仓库的子仓库。或者说,可以实现一个主仓库包含多个子仓库。

subtree 的使用

查看git subtree --help,发现命令如下:

1
2
3
4
5
6
git subtree add   -P <prefix> <commit>
git subtree add -P <prefix> <repository> <ref>
git subtree pull -P <prefix> <repository> <ref>
git subtree push -P <prefix> <repository> <ref>
git subtree merge -P <prefix> <commit>
git subtree split -P <prefix> [OPTIONS] [<commit>]

测试

使用以下简写来代表仓库名称:

  • A:主仓库
  • B:子仓库
  1. 首先新建一个A仓库,进入A的主目录,在A仓库中执行命令(假定所有子仓库目录都存在于A/src中)
    1
    git subtree add -P src/B https://github.com/B.git master

此时subtree则在A仓库中绑定了B仓库

  1. 进入A/src/B

    1
    touch test
  2. A根目录提交commit

    1
    2
    3
    git add src/B/test
    git commit -m "subtree test"
    git push origin master

那是如何同步相关commit到子仓库呢?subtree push即可

1
git subtree push -P src/B https://github.com/B.git master

即此,已经完成A,B仓库相关commit同步

优化

如果,A/src中有很多子仓库,每次更新完成后,手动更新N个子仓库非常让人头疼,索性搞个Bash跑一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env bash
set -euo pipefail

TARGET_DIRECTORY="$(pwd)/src"

function git.branch {
br=`git branch | grep "*"`
echo ${br/* /}
}
BRANCH=$(git.branch)

# each所有目录
for file in `ls ${TARGET_DIRECTORY}`
do
if [[ -d "${TARGET_DIRECTORY}/${file}" ]]; then
echo -e "==================Current:${file}==================\n"
echo `git subtree push -P src/${file} git@github.com:${file}.git ${BRANCH}`
echo -e "\n"
fi
done

Composer replace

可以看到在laravel/composer.json中有replace属性

1
2
3
4
5
6
"replace": {
"illuminate/auth": "self.version",
"illuminate/broadcasting": "self.version",
"illuminate/bus": "self.version"
......
}

那么如果是PHP情况下我们也可以,使用replace来在主包中加载子包依赖,从而提升统一版本更新的便捷性。

相关资料

Composer Replace使用
Git subtree使用

Laravel env 下特殊字符的坑

简述

今天在部署服务器时,线上数据库使用了特殊字符,结果导致mysql连接一直是出错的状态。
通过my-cli连接却是OK,于是想到是否是帐号密码错误,打印下envconfig,发现因为出现了#字符,后面被自动截断。

解决

第一反应是对于特殊字符如# \,应该加上"",测试下确实得到解决。

调试

首先想到肯定是在加载env,定位Bootstrap/LoadEnvironmentVariables

1
2
3
4
5
try {
$this->createDotenv($app)->safeLoad();
} catch (InvalidFileException $e) {
$this->writeErrorAndDie($e);
}

继续往下,定位safeLoad

1
2
3
4
5
6
7
8
9
public function safeLoad()
{
try {
return $this->loadData();
} catch (InvalidPathException $e) {
// suppressing exception
return [];
}
}

再通过loadData往下:

1
2
3
4
5
6
public function load()
{
return $this->loadDirect(
self::findAndRead($this->filePaths)
);
}

最终通过processEntries定位,找到处理value的核心方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
private static function parseValue($value)
{
if ($value === null || trim($value) === '') {
return $value;
}

return array_reduce(str_split($value), function ($data, $char) use ($value) {
switch ($data[1]) {
case self::INITIAL_STATE:
if ($char === '"' || $char === '\'') {
return [$data[0], self::QUOTED_STATE];
} elseif ($char === '#') {
return [$data[0], self::COMMENT_STATE];
} else {
return [$data[0].$char, self::UNQUOTED_STATE];
}
case self::UNQUOTED_STATE:
if ($char === '#') {
return [$data[0], self::COMMENT_STATE];
} elseif (ctype_space($char)) {
return [$data[0], self::WHITESPACE_STATE];
} else {
return [$data[0].$char, self::UNQUOTED_STATE];
}
case self::QUOTED_STATE:
if ($char === $value[0]) {
return [$data[0], self::WHITESPACE_STATE];
} elseif ($char === '\\') {
return [$data[0], self::ESCAPE_STATE];
} else {
return [$data[0].$char, self::QUOTED_STATE];
}
case self::ESCAPE_STATE:
if ($char === $value[0] || $char === '\\') {
return [$data[0].$char, self::QUOTED_STATE];
} else {
throw new InvalidFileException(
self::getErrorMessage('an unexpected escape sequence', $value)
);
}
case self::WHITESPACE_STATE:
if ($char === '#') {
return [$data[0], self::COMMENT_STATE];
} elseif (!ctype_space($char)) {
throw new InvalidFileException(
self::getErrorMessage('unexpected whitespace', $value)
);
} else {
return [$data[0], self::WHITESPACE_STATE];
}
case self::COMMENT_STATE:
return [$data[0], self::COMMENT_STATE];
}
}, ['', self::INITIAL_STATE])[0];
}

可以看到对于#\的处理。

关于array_reduce函数,请参考文档

Golang Interface 基础示例一

路径interfaces/demo.go

1
2
3
4
5
6
7
8
9
10
11
12
package interfaces

type Summationer interface {
Sum(param1, param2 int) int
}

type Summation struct {
}

func (s *Summation) Sum(param1, param2 int) int{
return param1 + param2
}

main调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"interfaces"
)

func main() {
// Go的接口是动态的

// 申明一个结构体
sum_struct := new(interfaces.Summation)

// 给当前结构体对象增加一个接口
//var sumInter interfaces.Summationer
//sumInter = sum_struct

// 或者使用如下方法
// sumInter := interfaces.Summationer(sum_struct)

// 或者,这样其实接口也是通的,但并没有什么太大意义
// 语法上看不出来结构体实现了此接口
sumInter := sum_struct

fmt.Println(testInterface(sumInter))
}

func testInterface(s interfaces.Summationer) int {
return s.Sum(1, 2)
}