在动态值为 nil 的接口上调用方法

把一个值为nil的某个实现类型的变量赋给了接口变量,是否可以在这个接口变量上调用该接口的方法

lib/lib.go

type People struct {
	Name string
	Age  int
}

type Superman struct {
	People
	color string
}

type Mutant interface {
	Speaking()
}

type Fly interface {
	Fly()
}

func (sp Superman) Speaking() {
	fmt.Printf("I'm %s", sp.Name)
}

func (sp *Superman) Fly() {
	fmt.Println("I can fly")
}

value.go

func callMethodOnNil() {
	var mu lib.Mutant
	var xman *lib.Superman

	fmt.Println("xman:", xman) // nil
	mu = xman
	fmt.Println("mu:", mu) // nil
	// Panic: runtime error: invalid memory address or nil pointer dereference
	// 动态类值为 nil 时,不能在其上调用值方法
	// mu.Speaking()

	var fly lib.Fly
	fmt.Println("xman:", xman) // nil
	fly = xman
	fmt.Println("fly:", fly) // nil

	// 动态类值为 nil 时,可以在其上调用指针方法
	// Fly()方法可以被调用,它的接收者是 *T 类型,且方法中没有涉及到对类型属性的调用
	fly.Fly() // I can fly
}

小结

  1. 值类型(T)的方法不能被调用,无论这个方法中是否涉及了对类型属性的调用

  2. 指针类型(*T)的方法可以被调用,但方法中不能涉及类型属性的调用

如何让主 goroutine 在所有其他 goroutine 都运行完后,再退出

sleep

通过 sleep 让主 goroutine 在所有子goroutine运行结束后,再退出

func usingSleep() {
	for i := 0; i < 10; i++ {
		go fmt.Println(i)
	}
	time.Sleep(time.Second)
}

缺点:无法判断 for 循环到底需要多长时间,导致估计sleep的时间要么不够,要么太长

缓冲通道

通过缓冲通道让主 goroutine 在所有子goroutine运行结束后,再退出

func usingChannel() {
	c := make(chan struct{}, 10)
	for i := 0; i < 10; i++ {
		go func(i int) {
			fmt.Println(i)
			c <- struct{}{}
		}(i)
	}

	for i := 0; i < 10; i++ {
		<-c
	}

}

缺点:

  • channel应该被用在goroutine间的通信
  • 如果子goroutine太多,使用通道也会消耗很多资源

sync.WaitGroup

使用 sync.WaitGroup()控制主 goroutine 在所有子goroutine运行结束后,再退出

func waitGroup() {
	wg := sync.WaitGroup{}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println(i)
		}(i)
	}

	wg.Wait() // 一直阻塞主goroutine,直到wg为零

	// output
	// 乱序输出,并不是按1,2,3,4...
	// 2
	// 4
	// 7
	// 3
	// 1
	// 0
	// 9
	// 5
	// 8
	// 6
}
  • Wait() 方法的功能是,阻塞当前的 goroutine,直到其所属值中的计数器归零

  • 不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行,如果同时启用的两个 goroutine ,分别调用这两个方法(add 和 wait),那么就有可能会让这里的Add方法抛出一个 panic。最好用“先统一Add,再并发Done,最后Wait”这种标准方式,来使用WaitGroup值

  • 计数器不能为负值,发生为负的情况是:不适当地调用Done方法和Add方法都会如此。

  • Done()是Add(-1)的别名

  • 计数不为0, 阻塞Wait()的运行

WaitGroup对象不是一个引用类型

在通过函数传值的时候需要使用地址,不然会出现 fatal error: all goroutines are asleep - deadlock!

func passWGByPointer() {
	wg := sync.WaitGroup{}

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go wgDone(i, &wg)
	}
	wg.Wait()

}
func wgDone(i int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println(i)
}

合并还是变基

合并分支时,应使用rebase合并分支而不是使用merge

merge

在分支很多的时候,merge会使提交变得异常混乱,查找某个提交会非常不好找,即使使用图形工具,也无法快速的查找,如果想退到某次提交更是难上加难。merge即使删除了合并过来的分支,其合并轨迹也依然被保留,所以还是很乱。

git merge

rebase

在分支很多的时候,不会产生很多的分支线,实践中应该就只有master,release等分支线,功能分支线在合并到master 上后都被删除,这样提交基本都在一条线上,十分清楚整洁。

git rebase

使用rebase合并分支

场景分析:

功能feature1feature2都在开发,feature1先上线,合并到了master,此时,feature2不应使用mergemaster进行合并操作,而是使用rebase进行合并

  • master上的提交有:[m1, m2, m3]

  • feature1上的提交有: [f1_01, f1_02, f1_03]

  • feature2上的提交有: [f2_01, f2_02, f2_03]

feature1先被合并到master:

  • master上的提交有:[m1, m2, m3, f1_01, f1_02, f1_03]

feature2合并master

  • feature2上的提交有:[m1, m2, m3, f1_01, f1_02, f1_03,f2_01_new, f2_02_new, f2_03_new]

合并步骤

feature2要把master上的提交合并过来,使用rebase 而不是merge,这样就会不产生合并提交,在向master提交时就变成了快速向前

git co feature2
git rebase master

# 如果发生冲突,解决冲突后把冲突文件加入 index 区
git add -u
# 运行continue 以使rebase完成
git rebase --continue
  • 步骤1:git rebase master:以master分支做为feature2的基础

  • 步骤2:计算出feature2master上的差异提交,相当于git log master...feture2,即[f2_01, f2_02, f2_03]

  • 步骤3:把[f2_01, f2_02, f2_03]master上的最新提交f1_03上进行重放生成新的提交

    [f2_01_new, f2_02_new, f2_03_new]

    重放生成过程:

    1. f2_01以f1_03为基础,生成新的f2_01_new
    2. f2_02以f2_01_new为基础,生成新的f2_02_new
    3. f2_03以f2_02_new为基础,生成新的f2_03_new
  • 步骤4:把当前分支设置为feature2

    feature2rebase后的提交有[m1, m2, m3, f1_01, f1_02, f1_03,f2_01_new, f2_02_new, f2_03_new]

    之前的旧的[f2_01, f2_02, f2_03]feature2上就看不到了,但依然在git中,如果没有其它分支指向它们,则会被git 回收机制删除

小结

  1. merge会让提交变得越来越混乱
  2. rebase可以使主线一直保持清晰,rebase操作可以看是剪支操作,把分支剪下来,接到主线上。这样就可以把多条分支线变为一条,使所有提交变得清晰。
  3. 不要对已经push了的分支进行rebase,这会让已经存在的提交丢失,如果有人已经拉取了这个分支,也会给别人提交带来混乱

自增值申请策略

  1. 能否先查ID,再查入数据?

    不能,如果每次插入前,都要在主键的索引树上查询要插入的id是否存在,会大大降低插入插入的效率。

  2. 能否等事务完成后,再释放自增值的锁?

    不能,如果需要等待当前事务完成,其它事务先能获取自增值的锁,就会大大降低并发。

innodb_autoinc_lock_mode

MySQL通过参数innodb_autoinc_lock_mode,来设置自值锁的行为,默认值为1

  1. 0值 :等事务完成才释放锁

  2. 1值 :

    • 对于普通的insert into table,申请到自增锁后就释放,不用等插入语句的完成。
    • 对于批量插入,类似insert ... select,一直持有自增锁,直到事务执行完成。这样也保证了id的连续性。
  3. 2值:所有插入操作在申请到自增锁后就释放,包括insert ... select 这种

主goroutine先执行完,子goroutine居然还可以运行

func main() {
	ch := make(chan int)
	log.Println("===========")
	go func() {
		log.Println("doing in sub goroutine")
		// 主程序向通道发送数据,但此时由于 主 gor 行速度慢
		// 程序还没有执行到 fmt.Println("x in ch is:", <-ch),
		// 通道还没有接收数据,所以此时发生 阻塞
		ch <- 1
		log.Println("done in sub goroutine") // 竞争打印,顺序不定
	}()

	// 模拟执行速度慢
	time.Sleep(1 * time.Second)

	log.Println("doing in main goroutine")
	// 1. 执行到这里时发生,由于子 gor 已经在等待向通道发送数据
	//    所以此处的 从通道接收数据的 <-ch 立即执行
	// 2. 子 gor 被唤酲, 立即向通道发送数据
	// 3. <-ch 开始接收数据

	// 当从通道中取出数据时,数据是通道中复制的,这需要花费一定的时间
	fmt.Println("x in ch is:", <-ch)      // 竞争打印,顺序不定
	log.Println("done in main goroutine") // 竞争打印,顺序不定
}

output:这个比较奇怪,主 先执行完,子居然还可以运行

// 2021/03/04 16:26:35 doing in sub goroutine
// 2021/03/04 16:26:36 doing in main goroutine
// x in ch is: 1
// 2021/03/04 16:26:36 done in main goroutine (主 先执行完)
// 2021/03/04 16:26:36 done in sub goroutine (子居然还可以运行)

关于原因

  1. Stack Overflow上的答案是 在主 gor 的最后一行代码

    log.Println("done in main goroutine") 执行完成后,main 所在的 goroutine 还有一些在 runtime

    需要完成一些扫尾工作,这些扫尾工作会花费非常少的时间,在这段时间里,子 gor 依然可以运行

    所以可以执行 log.Println("done in sub goroutine")

  2. 这也说明了 此次 主 gor 运行的比较慢,给子gor 留下了运行完所有代码的时间

    如果主 gor 运行的特别快,即使扫尾工作需要花费一定的时间,但整体速度依然比

    子 gor 快,则 log.Println("done in sub goroutine") 不能被执行

行锁

优点:

比起表锁,可以支持更大的并发

行锁的实现

由存储引擎自已实现,每个存储引擎实现的方式都不一样

MyISAM 引擎不支持行锁

两阶段锁

在更新数据时,需要对数据所在行加锁,但更新数据完成后,锁并不会马上释放,而是要等到事务提交才释放

create table t_07_recordlock(
	id int not null auto_increment primary key,
  c int,
  d int,
 	key c (c)
) engine=innodb;

insert into t_07_recordlock values (1,1,1),(2,2,2),(3,3,3),(4,4,4),(5,5,5);
Session A Session B
T1 Begin; begin;
T2 update t_07_recordlock set d=d+1 where id = 1;
update t_07_recordlock set d=d+1 where id = 2;
T3 update t_07_recordlock set d=d+1 where id = 1;
T4 Commit
  • T2时刻,SessionA已经完成了对两行数据的更新,但没有提交
  • T3时候,SessionB对id=1的行进行更新,由于Session A没有提交,其依然持有id=1上的写锁,与Session B要加的写锁互斥,所以SessionB被阻塞,直到Session A提交,释放锁,才能对执行更新语句

MySql 主键不连续的原因之一是自增值不能回退

设当前自增值=2,此时并发两个事务T1,T2。T1先申请到自增值,插入ID=3,自增值更为4

Tx1 Tx2
T1 申请到自增值=2,set id =2, 更新自增值=3
T2 插入数据失败 申请到自增值=3,set id=3,更新自增值=4
T3 如果让自增值回退到=2 插入数据成功
T4 申请到自增值=2,set id =2, 更新自增值=3
T5 插入数据成功 申请到自增值=3,set id=3,更新自增值=4
T6 插入数据失败,插入语句报错“主键冲突” id=3已经存在

无缓冲通道的cap是零

package main

import (
	"fmt"
	"time"
)

// 无缓冲通道的cap、len永远是0
func main() {
	noCache := make(chan int)

	go func() { noCache <- 6 }()

	time.Sleep(2 * time.Second)
	fmt.Println("cap of noCache pipeline is", cap(noCache)) // => 0
	fmt.Println("len of noCache pipeline is", len(noCache)) // => 0

	fmt.Println("noCache ", <-noCache)
}

补充

  1. git 可以只提交文件的部分内容, git add -p,称之为hunk, sourcetree也可以进行设置

  2. 分支改名
    git branch -m cat tiger

  3. 分支合并后产生的commit, A合并B,commit 信息中,父提交记录顺序,parents : A, B

    A在前,B在后,表示是A 合并的B

  4. git如何判断分支是否可以删除
    当前分支所指向的commit ,没有其它commit指向它(这个提交(分支)还没有被合并),或只有当前分支指向它,没有其它分支指向它时,在删除这个分支时就会有提示不能直接删除

  5. rebase 后,对应commit 值是否发生变化
    看情况,嫁接到其它commit 之上的commit 的值会发生变化,因为其父提交发生了变化,同理,在这个commit 之后的提交的值也发生变化
    A B C D E F,把F嫁接到其它 COMMIT后,A到E的COMMIT的值都发生变化

    修改现有COMMIT 的注释,也会发生这样的连锁反应

  6. rebase 的其他用途
    修改历史提交注释 r选项
    合并多个提交 s 选项
    把一个提交拆分成多个commit 在操作列表使用使用 edit
    在commit 之间添加新的commit edit选项
    调整commit 之间的顺序 直接在列表里调整,
    删除commit 在列表里删除相关行过使用drop,删除后对它之后commit 的影响如何

  7. 空目录无法被添加到git
    因为根据算法,添加的对象必须要有内容,以供sha1算法使用

  8. 合并后再使用 git rebase 使提交成为一条直线

    git merge upstream-master
    Auto-merging layout/_partials/sidebar/site-overview.njk
    Auto-merging layout/_partials/post/post-followme.njk
    Merge made by the 'recursive' strategy.
     layout/_partials/header/menu-item.njk      | 5 +----
     layout/_partials/page/breadcrumb.njk       | 6 +-----
     layout/_partials/post/post-followme.njk    | 2 +-
     layout/_partials/sidebar/site-overview.njk | 2 +-
    
    git slog --graph
    *   b1b3e13 (HEAD -> release) Merge branch 'upstream-master' into release
    |\
    | * d51ca0e (upstream/master, upstream-master) Fix: handle invalid config
    * | 369c5b5 (origin/release, origin/master, origin/HEAD, master) Resolve conflicts
    |/
    * d6f344f Refactor sidebar-panel using flex layout
    * edefcda Add NProgress (#79)
    
    # 把前分支作为基础分枝, 把 upstream-master 分支的提交嫁接在其上面
    git rebase
    First, rewinding head to replay your work on top of it...
    Applying: Fix: handle invalid config
    Using index info to reconstruct a base tree...
    M	layout/_partials/post/post-followme.njk
    M	layout/_partials/sidebar/site-overview.njk
    Falling back to patching base and 3-way merge...
    Auto-merging layout/_partials/sidebar/site-overview.njk
    Auto-merging layout/_partials/post/post-followme.njk
    
    git slog --graph
    * 217e224 (HEAD -> release) Fix: handle invalid config
    * 369c5b5 (origin/release, origin/master, origin/HEAD, master) Resolve conflicts # 以这个提交做为 rebase
    * d6f344f Refactor sidebar-panel using flex layout
    * edefcda Add NProgress (#79)
    1. 使用merge出现了分支线路
    2. rebase后,分支线路消失,成为一条直线。 是 以当前分支的最TOP的那个(369c5b5)提交做为rebase,重放合并进来的分支上的提交的

What does kill -0 ... do?

if ! kill -0 $(cat /path/to/file.pid); then
    ... do something ...
fi

kill(1)

$ man 1 kill
...
If sig is 0, then no signal is sent, but error checking is still performed.
...

kill(2)

$ man 2 kill
...
If sig is 0, then no signal is sent, but error checking is still performed;
this can be used to check for the existence of a process ID or process
group ID.
...

So signal 0 will not actually in fact send anything to your process’s PID, but will check whether you have permissions to do so.

Where might this be useful?

One obvious place would be if you were trying to determine if you had permissions to send signals to a running process via kill. You could check prior to sending the actual kill signal that you want, by wrapping a check to make sure that kill -0 <PID> was first allowed.

Example

Say a process was being run by root as follows:

$ sudo sleep 2500 &
[1] 15693

Now in another window if we run this command we can confirm that that PID is running.

$ pgrep sleep
15693

Now let’s try this command to see if we have access to send that PID signals via kill.

$ if ! kill -0 $(pgrep sleep); then echo "You're weak!"; fi
bash: kill: (15693) - Operation not permitted
You're weak!

So it works, but the output is leaking a message from the kill command that we don’t have permissions. Not a big deal, simply catch STDERR and send it to /dev/null.

$ if ! kill -0 $(pgrep sleep) 2>/dev/null; then echo "You're weak!"; fi
You're weak!

Complete example

So then we could do something like this, killer.bash:

#!/bin/bash

PID=$(pgrep sleep)
if ! kill -0 $PID 2>/dev/null; then
  echo "you don't have permissions to kill PID:$PID"
  exit 1
fi

kill -9 $PID

Now when I run the above as a non-root user:

$ ~/killer.bash
you don't have permissions to kill PID:15693

$ echo $?
1

However when it’s run as root:

$ sudo ~/killer.bash

$ echo $?
0

$ pgrep sleep
$