1 零碎知识点
1.1 本地doc文档
在终端执行godoc -http=:6060
将本地的godoc文档发布到端口6060上,即可访问本地的godoc文档。
1.2 捕获dump信息
利用"runtime/debug"
的包中stack函数捕获,将捕获的信心持久化到dump文件中:
func dump() {
rcv_err := recover()
if rcv_err == nil {
return
}
now := time.Now()
strTime := now.Format("20060102150405")
name := fmt.Sprintf("dump-%s.log", strTime)
fmt.Println("dump file in", name)
f, err := os.Create(name)
if err != nil {
return
}
defer f.Close()
f.WriteString(fmt.Sprintf("panic:\r\n%v", rcv_err))
f.WriteString("\r\n\nstack:\r\n")
f.WriteString(string(debug.Stack()))
}
1.3 "os/exec"启动进程
exec包CommandContext
启动进程。
var cmd *exec.Cmd
ctx, _ := context.WithCancel(context.Background())
if len(serviceArgs) == 0 {
cmd = exec.CommandContext(ctx, servicePath)
} else {
cmd = exec.CommandContext(ctx, servicePath, serviceArgs)
}
var err error
go func() {
defer cmd.Wait()
if err = cmd.Start(); err != nil {
return
}
}()
需要注意的是CommandContext
的第一个参数不能为WithTimeout
之类的,这种情况下启动进程会在到时间后直接退出,必须用WithCancel(Backgroud)
的形式.
Run
是阻塞的形式
Start()
非阻塞模型,需要用wait
等待,一般情况下启动进程肯定是需要被启动进程一直运行的,这种情况下wait
会阻塞住,大部分情况用go func(){}
的形式启动进程。
1.4 观察者模式
当一种对象变化时通知另一个对象变化得到事件。定义了2个接口:观察者接口和备观察者接口。除此之外又定义了一个通知的时间结构体event
,可以在这个event
中增加事件的更多参数。
type event struct {
uuid string
}
1.4.1 观察者
1.4.1.1 接口定义
定义一个函数,用于接受被观察者的事件通知
// 被观察者接口
type observer interface {
Update(*event)
}
1.4.1.2 观察者实例
观察者实现了接口函数Update
用于观察通知事件;又定一个一个辅助函数,addJob
type task struct {
jobs []job
}
func (t *task) Update(e *event) {
log.Printf("更新事件uuid=%v",e.uuid)
}
func (t *task) addJob(j job) {
t.jobs = append(t.jobs, j)
}
1.4.2 被观察者
1.4.1.1 接口定义
定义三个函数,给当前被观察者增加一个观察者,删除当前被观察者的观察者和通知观察者变更事件。
// 被观察者接口
type subject interface {
Attach(...observer) // 添加观察者
Detach(observer) // 删除观察者
Notify(string) // 通知观察者
}
1.4.1.2 被观察者实例
type job struct {
observers []observer // 观察者对象
}
func (j *job) Attach(obs ...observer) {
j.observers = append(j.observers, obs...)
}
func (j *job) Detach(observer) {
}
func (j *job) Notify(uuid string) {
for _,obs:=range j.observers {
obs.Update(event{"123"})
}
}
1.4.3 主程序调用
定义了一个task观察者对象和job被观察者对象,当job变更时通知task对象。
package main
import "log"
func main() {
t := new(task)
j := job{}
log.Printf("0-------[%+v]", j)
j.Attach(t)
log.Printf("1-------[%+v]", j)
t.addJob(j)
log.Printf("2-------[%+v]", t)
}
1.5 go程序的版本
编译的go程序执行./demo -v
时打印当前的版本
package main
import (
"fmt"
"log"
"os"
"runtime"
"strings"
)
var (
version = ""
githash = ""
buildstamp = ""
goversion = fmt.Sprintf("%s %s/%s", runtime.Version(), runtime.GOOS, runtime.GOARCH)
)
func Version() {
args := os.Args
if len(args) == 2 {
arg := strings.ToLower(args[1])
if arg == "--version" || arg == "-v" {
fmt.Printf("version: %s\n", version)
fmt.Printf("Git Commit Hash: %s\n", githash)
fmt.Printf("UTC+8 Build Time : %s\n", buildstamp)
fmt.Printf("Golang Version : %s\n", goversion)
os.Exit(0)
}
}
}
func main() {
Version()
log.Printf("hello world")
}
Makefile的实现,当直接make
时,先执行go test
运行所有的test用例,然后执行go build
编译文件。编译后的程序版本信息中默认了version=0.0.1
,date为编译的时间,gitbash
为git log的commit数字,还有go的编译版本。
执行make build 时
只编译,不运行编译的测试用例。手动加版本为1.0.0-stable
时运行make build version=1.0.0-stable
,编译的程序就有了对应的版本号。
# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
BINARY_NAME=demo
# version infos
gitbash:=$(shell git rev-parse HEAD)
version:=0.0.1
date:=$(shell date "+%G%m%d%H%M%S")
# compile flags,default can not use gdb debug
flags:=-w -s
DFLAGS:=
ifeq ($(DFLAGS),debug)
flags=
endif
all:test build
build:
$(GOBUILD) -v -ldflags '-X "main.version=${version}" -X "main.buildstamp=${date}" -X "main.githash=${gitbash}" ${flags}' -o ${BINARY_NAME}
test:
$(GOTEST) -v ./...
clean:
$(GOCLEAN)
rm -f $(BINARY_NAME)
run:
$(GOBUILD) -o $(BINARY_NAME) -v ./...
./$(BINARY_NAME)
help:
@echo "************* help *************"
@echo "* make : make test and make build for all services "
@echo "* make test : make test for all services "
@echo "* make build: make build all services,if there are errors during compilation, look for the error message in the file builderr.txt"
@echo "* make clean: remove all executable programs and intermediate temporary files generated by the compilation "
deps:
$(GOGET) github.com/xxx/xx
$(GOGET) github.com/xxx/xx
.PHONY: all build test clean help
make build DFLAGS="debug"
编译为debug版本,可以gdb调试。
1.6 压缩go的可执行程序大小
默认的情况下编译出来的go可执行程序会非常大,动辄就是几十兆起,可以通过一些策略来尽可能的减小文件的大小。代码示例:
package main
import "fmt"
func main(){
fmt.Println("hello world")
}
1.6.0 直接编译文件
go build -o main main.go
编译后文件大小:2068291
字节
1.6.1 编译参数
1 编译参数
编译时增加-ldflags中的-s和-w参数,会去掉“符号表”
和“ DWARF generation”
,生成的文件会减小一半左右,编译:
go build -ldflags '-s -w' -o main.release main.go
编译后:1458176
字节,压缩率:(2068291-1458176)/2068291*100=29.46%
查看-ldflags参数的详细信息可以通过go tool link
查看,以下为部分参数:
2 strip删除符号变和节
这种方式其实和linux自带的命令strip
效果一致,作用是:从文件中删除符号和节。以下是参数:
如果对于一个直接编译的go可执行程序,使用这个命令处理后减小的文件大小和增加编译项的大小差不多一致:
strip main -p -o main.strip
以上将源文件mian
去掉符号和节后文件名为main.strip
,执行后的文件:1455544
字节,压缩率:(2068291-1455544)/2068291*100=29.62%
这两种任选其一即可,一般我们使用***编译参数控制***,即可以生成可以debug的,也可以生成release的版本
1.6.2 文件加壳压缩
在ubuntu和centos下安装upx
ubuntu下如果使用国内的源的话可以直接安装:sudo apt install upx
centos下安装:
wget -c http://ftp.tu-chemnitz.de/pub/linux/dag/redhat/el7/en/x86_64/rpmforge/RPMS/ucl-1.03-2.el7.rf.x86_64.rpm
rpm -Uvh ucl-1.03-2.el7.rf.x86_64.rpm
yum install ucl
wget -c http://ftp.tu-chemnitz.de/pub/linux/dag/redhat/el7/en/x86_64/rpmforge/RPMS/upx-3.91-1.el7.rf.x86_64.rpm
rpm -Uvh upx-3.91-1.el7.rf.x86_64.rpm
yum install upx
安装完成后执行upx如下:
1 直接压缩原始文件
将原始文件main
高质量压缩:
upx -9 main -o main.upx
生成的文件大小:1129624
字节,压缩率:(2068291-1129624)/2068291*100=45.38%
2 直接压缩编译优化的文件
upx -9 main.release -o main.release.upx
生成的文件大小:582140
字节,压缩率:(2068291-582140)/2068291*100=71.85%
3 strip已经压缩的文件是不允许的
strip main.upx -p -o main.upx.strip
由于在upx加壳压缩时已经将符号表和节内容删除了,再次strip时报了错
1.6.3 减小文件大小的小结
1 带参编译,直接去掉符号表等参数-w -s
2 使用upx加壳压缩文件,可以使用高质量-9参数压缩,压缩比可达70%左右
2、面试知识点
2.1 基础知识
2.1.1 关键字
1) new和make的区别
new
内建函数,初始化一个指向类型的指针(*T),传递给new的是一个类型,返回的是这个新分配的零值的指针
make
内建函数,作用于slice,map或chan,初始化并返回引用(T),返回一个初始化的实例
2 ) 数组和切片的区别
数组是值传递,内置类型;具有固定的长度且拥有0个后者多个相同数据类型元素的序列。
切片,表示拥有相同类型元素的可变长度的序列,有三个属性:指针,长度和容量,不需要指定大小,是地址传递,在追加元素时如果容量cap不足时将按len的2倍扩容
3)go中的引用类型
包括,数据切片、字典、通道、接口interface
4)go触发panic的场景
使用空指针,下标越界,调用panic函数
5)defer函数
defer函数属于延迟函数执行,多个defer函数之间按照LIFO先进后出的顺序执行
2.1.2 堆和栈的问题
i. go语言的编译器会选择把一个变量放在堆上还是栈空间上,编译器会做逃逸分析,当编译发现变量的作用域没有逃逸函数的范围,就可以在栈上,反之则必须分配在堆上
ii. 以下情况,变量一定被分配到堆上
a.如果变量被取地址,并且被逃逸分析识别为“逃逸到堆”
b.如果一个变量很大
2.1.3 性能调试
i. 查看panic的调用栈
ii. pprof
iii. 火焰图(配合压力测试)
iv. 使用go run -race 或者 go build -race 来进行竞争检测
2.2 channel的特性
1)给nil的channel写数据,永远阻塞
2)从一个nil的channel读数据,永远阻塞
3)给一个close的channel写数据,引起panic
4)从关闭的channel中读数据,如果缓冲区为空,返回一个nil
5)无缓冲的channel是同步的,而有缓冲区的channel是异步的
ch := make(chan interface{}) 和 ch := make(chan interface{},1)的区别?
无缓冲的必须要一直有接收者才行,要么会阻塞数据写入;
有缓冲的当写入的一个值尚未被拿走的时候,写数据失败,才会被阻塞。
2.3. 协程的调度原理
2.3.1 什么是协程
协程拥有自己的寄存器和上下文栈。协程调度切换时保存寄存器和上下文状态,再次切换回来时恢复寄存器和上下文。这个切换是在go的runtime调度的,无需系统内核切换,消耗的资源(cpu)就很小了,因此比较线程和进程来说,协程拥有强大的并发能力。
2.3.2 go的调度器——GPM模型
go的调度器内部有四个重要的结构:M、P、S、Sched:
M:表示内核级线程,一个M就是一个线程,goroutine就是运行在M之上的;M内部维护了许多小对象内存cache、当前执行的goroutine、随机数发生器等
G:表示一个goroutine,它拥有自己的栈,以及其他的信息,用于调度
P:Processor,主要用来直接goroutine,维护了一个goroutine队列,里面存储了所有需要执行的goroutine
Sched:调度器,他维护存储有M和G的队列以及调度器的一些状态信息
3)调度实现
2.3.3 gc的理解
未完待续。。。