一. Hello Go
每个语言入门的第一行代码莫过于hello world 编写如下代码并执行 go run 文件名.go
package main
import "fmt"
func main() {
fmt.Println("hello world!")
}
这段代码需要注意的点:
- 所在包必须是
main 包,在java中是没有这个必要的 - 方法也必须是
func main() ,不能加参数返回值这些东西 - 文件名可以随便,只要是
xxx.go 即可,所在的目录名称也不需要是包名
这里如果想获取到返回状态怎么办呢?类似于C语言中return 0; ,可以使用os.Exit(0) 方法,与shell 等一样,传0代表真,其它数字代表假。在命令行中可以使用$? 获取运行状态,当然程序运行结束也会展示出来。 这里代码最后就是os.Exit(1)
那么如果获取到main参数呢,类似于java的public static void main(String[] args) ,在go中想获取到类似的参数不能通过main函数中加参数的办法,需要通过os.Args 来获取,表示一个string类型的切片(切片是一种数组的封装,具有更好的灵活性)。如下:
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) > 1 {
fmt.Println("Hello World", os.Args[1])
}
}
os.Args[0] 是该文件生成的可执行文件的全路径
二. 变量与常量
变量的使用方式:
variable := 5
func main() {
var a int = 1
var b = 1
var (
c int = 2
d = 3
)
e := 1
}
这里go语言是支持一个赋值语句给多个变量同时赋值的,如下:
func main() {
a := 1
b := 2
a, b = b, a
}
常量的使用方式:
const cst = "这是个常量字符串"
const (
multi1 = "这是一次声明多个常量的方式"
multi2 = "第二个常量"
)
const (
sameVal1 = "这是一段话"
sameVal2
)
这里有个好用的常量iota ,它的默认值为0,但是遇到了声明在一起的常量他会自增,具体实例如下:
const (
a = 1
b = iota
c
)
const (
v1, v2 = iota, iota
v3, v4
)
func main() {
fmt.Println(a, b, c)
fmt.Println(v1, v2, v3, v4)
}
三. 数据类型
基本的数据类型有如下:
类型 | 表示 |
---|
布尔型 | bool | 字符串 | string (go的字符串并不像java一样属于引用,默认为""而不是nil | 整型 | int int8 int16 int32 int6 (int是int32还是int64取决于运行的机器位数) | 无符号整型 | uint uint8 uint16 uint32 uint64 uintptr(这是个指针类型) | 比特 | byte (是uint8的别名) | rune | rune(是int32的别名,每一个rune代表了一个unicode coed point ) | 浮点型 | float32 float64) | 复数 | complex64 complex128 |
注意点:
- go语言不允许隐式类型转换
- 即使这两个类型的实现完全相同也不可以
- 指针类型是不支持运算的
#include <iostream>
using namespace std;
int main() {
int a = 100;
long long int b = a;
return 0;
}
在C语言里面是可以上面的操作的,但是go语言不可以
package main
import "fmt"
type myInt int
type aliasInt = int
func main() {
a := 100
var b myint
b = a
fmt.Println(b)
var c aliasInt
c = a
fmt.Println(c)
}
func main() {
a := 1
c := &1
fmt.Printf("c的类型是%T\n", c)
var array = []int{1, 2, 3}
arrayAddress := &array
fmt.Printf("%v\n", unsafe.Pointer(arrayAddress))
}
小tips: ? ? 类型都存在一些预定义的最大值最小值,在math模块中 ? ? math.MaxInt64 ? ? math.MaxFloat64 ? ? 等等…
四. 运算符
基本与c++ java 等语言一致,几点区别如下:
- 无前自增和前自减(++a --a)
- 可以用
== 比较数组,但是要求数组长度完全相同才能比较(每个位置的元素都一样返回true) - 多了一个
&^ 运算符(按位 - 置零),规则如下:
func main() {
a := 3
b := 1
c := a &^ b
}
五. 结构化程序
5.1 循环
go语言不支持while 循环,只支持for 循环
func main() {
for i := 0; i < 5; i++ {
fmt.Println(i)
}
start, end := 1, 5
for start <= end {
fmt.Println(start)
start++
}
for {
fmt.Println("死循环....")
}
}
5.2 分支
func main() {
var a int
fmt.Scan(&a)
if a >= 1 && a <= 5 {
fmt.Println("输入的数字在1~5之间")
} else if a >= 6 && a <= 10 {
fmt.Println("输入的数字在6~10之间")
} else {
fmt.Println("输入的数字不在1~10之间")
}
}
go语言的if 可以使用两段表达式,缩小了一些不必要变量的作用域,非常方便
func main() {
if i := 5; i < 10 {
fmt.Println("i是小于10的")
}
}
switch语句使用如下:
func main() {
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("linux")
default:
fmt.Printf("other os, it's name is %s\n", os)
}
switch {
case 1 <= 3:
fmt.Println("恒真")
case 1 > 2:
fmt.Println("恒假")
}
}
注意点:
- 这里判断的可以不只是 常量和整数
- 单个case可以逗号分隔多个选项,不需要像
c 一样不写break让语句落下去 - go是不需要break掉case的,自动退出
六. 数组和切片
声明数组:
func main() {
var array [5]int
array[0] = 100
b := []int{1, 2, 3}
c := [2][3]int{{1, 2}, {3, 4, 5}}
d := [...]int{1, 2, 3}
}
数组遍历:
func main() {
a := [...]int{1, 2, 3, 4}
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}
for idx, elem : range a {
fmt.Println(idx, elem)
}
}
数组截取:
func main() {
a := [...]int{1, 2, 3, 4, 5}
part := a[1:3]
fmt.Println(part)
}
切片的内部结构: 切片的底层仍然维护了一个数组,维护了数组的头指针地址 + 切片的长度 + 底层数组的长度
这就会发现 (c = a[0:0] 相当于清空了,也可以直接c = nil,二者仍有区别,下面会对比) 这句话, 如果c = nil 那么我操作c,a是不会发生任何变化的,但是c = a[0:0],它们的底层共享同一个数组,我改了c的elem,a也会变化
声明一个切片:
func main() {
var slice0 []int
slice1 := []int{}
slice2 := make([]int, 2, 5)
slice2 = append(slice2, 99)
}
这里需要注意的地方是:cap() 函数可以求切片底层数组的长度,但不是整个数组的长度,如图:
数组与切片对比:
- 切片的容量可伸缩,数组不行(动态扩容的时候,会变成原来cap的二倍,能知道该声明多大时尽量固定容量)
- 数组可以比较,切片只能和
nil 进行比较
七. Map
7.1 Map基础
map的声明:
func main() {
m := map[int]string{
1: "一月",
2: "二月",
}
m1 := map[string]int{}
m1["lalala"] = 1
m2 := make(map[int[string, 10)
}
map的访问:
func main() {
m := map[int]string{
1: "一月",
2: "二月",
}
v := m[1]
fmt.Println(v)
v := m[3]
if val, ok := m[3]; ok {
fmt.Println(val)
} else {
fmt.Println("key不存在")
}
}
map的遍历:
func main() {
m := map[int]string{
1: "一月",
2: "二月",
}
for key, val := m {
fmt.Println(key, val)
}
}
7.2 Map扩展
map的value部分可以是个函数,所以可以轻松实现一个简单的工厂模式:
func main() {
myFactory := map[string]func(){}
myFactory["矿泉水"] = func() {
}
myFactory["饮料"] = func() {
}
}
实现set :
func mai() {
mySet := map[string]bool{}
mySet["myString"] = true;
_, ok := mySet["myString"]
if ok {
}
delete(mySet, "myString")
fmt.Println(len(mySet)
}
八. 字符串相关
- string是基本类型
- string是只读的
byte slice ,len() 可以求byte数,但是一个汉字占三个字节,所以无法直接求字符串长度 - string只读,用
+= 操作比较耗时 - 常用的字符串操作包:
strings strconv
func main() {
s := "A,B,C,D"
strSlices := strings.Split(s, ",")
for _, slice := range strSlices {
fmt.Println(slice)
}
toS := strings.Join(strSlices, "!")
fmt.Println(toS)
}
九. 函数
函数的基本格式:
func functionName(argA typeA, argB typeB) (resultA typeC, resultB typeD) {
return xx, xx
}
注意点:
- go语言的函数允许多返回值
- 所有的参数都是值传递,slice等也是,因为底层的数组指向没变,才有引用的错觉
- 函数可以作为参数传入和返回,当然也可以当成变量值
可变参数:
func variableArgsFunc(args ...int) int {
sum := 0
for _, num := args {
sum += num
}
return sum
}
func variableArgsFunc(args ...int) (sum int) {
for _, num := args {
sum += num
}
return sum
}
defer函数: 可以延迟函数的调用时机
func main() {
defer func() {
fmt.Println("final execute...")
}()
fmt.Println("第一个输出...")
}
如果有返回值,defer的时机是这样的:
func hasReturnFunc() int {
var n int
defer func() {
n = 9
}()
n = 10
return n
}
这个地方可能会有坑,这种形式的返回,其实返回的并不是n本身,而是它的一个副本。 按照上图的过程,坑产生在第一步,第一步其实是在复制一个副本出来,第二步改了n也没什么用,第三步返回的时候是defer执行前的副本,即答案为10 (千万不要认为他是在return n 这句话执行前执行defer,那样得到的应该是9)
当然还有另一个办法能让它变成9,写法如下:
func hasReturnFunc() (n int) {
defer func() {
n = 9
}()
n = 10
return n
}
只要在返回值的地方声明了次变量,那返回就是本身而不是副本
十. 面向对象编程
10.1 封装数据和行为
定义结构体:
type MyStruct struct {
elem string
name string
}
创建实例以及初始化:
func main() {
e := MyStruct{"elementVal", "jerry"}
e1 := MyStruct{elem: "elementVal", name: "jerry"}
e2 := &MyStruct{elem: "elementVal", name: "jerry"}
e3 := new(MyStruct)
}
方法的定义:
func (m MyStruct) introduce() string {
return "hello my name is " + m.name
}
上面的写法是m MyStruct ,实际上是一个实例副本,通常为了避免内存拷贝,用下面的方式:
func (m *MyStruct) introduce() string {
return "hello my name is " + m.name
}
10.2 Duck Type 式接口实现
接口的定义:
type myInterface interface {
Method() int
}
接口的实现:
var ok = 1
type myStruct struct {}
func (ms *MyStruct) Method() int {
fmt.Println("这是个接口的方法")
return ok
}
go语言并不像java一样,如果实现一个接口就需要求implements ,只要实现了接口定义的所有方法即可。这种方式是非入侵性的,并且不用先定义一堆接口。
注意: 空接口相当于java中的Object 类的作用,形参写空接口,实参可以传任意对象
type emptyInterface interface {}
10.3 扩展与复用
go是不支持继承的!!但是可以用复合的方式做到复用 用法如下:
type Pet struct {
}
type Dog struct {
Pet
}
func (p *Pet) say() {
fmt.Print("huhuhu~")
}
func main() {
dog := new(Dog)
dog.say()
}
多态的实现:只需要定义一个接口,让不同的结构体去实现这些方法,然后函数形参写接口即可
type sayInterface interface {
say()
}
type Pig struct {
}
type Dog struct {
Pet
}
func (p *Pig) say() {
fmt.Println("huhuhu~")
}
func (d *Dog) say() {
fmt.Println("wangwangwang")
}
func letPetSay(pet sayInterface) {
pet.say()
}
func main() {
dog := new(Dog)
letPetSay(dog)
}
10.4 空接口断言
当一个变量是接口类型的时候,我们可以通过断言将它转换为指定的类型:
type mInterface interface {}
type mStruct struct {}
func assertIt(i mInterface) {
if v, ok := i.(mStruct); ok {
fmt.Println("是mStruct类型")
}
}
func main() {
ms := new(mStruct)
assertIt(ms)
}
十一. 错误处理
11.1 error
go语言是没有java的那种try catch 异常机制的,主要通过errror 接口的传递来判断(error类型实现了error接口)
var numIsTooLargerError = errors.New("传入的数字过大")
func computeInTen(a int, b int) (int, error) {
if a > 10 || b > 10 {
return nil, numIsTooLargerError
}
return a + b, nil
}
这里错误处理我们应该尽早处理,防止过于多的错误情况出现嵌套,即:
func testError() {
if error1 {
return
}
if error2 {
return
}
}
而不是如下:
func testError() {
if !error1 {
if !error2 {
} else {
return
}
} else {
return
}
}
11.2 panic 和 recover
panic是用于不可恢复的错误,让程序crash 掉,但是panic后仍然会执行defer函数,我们可以尝试恢复一些已知的错误。常见的写法如下:
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("recovered from ", err)
}
}()
fmt.Println("Start")
panic(errors.New("Something wrong!"))
}
注意:当不知道错误是什么的时候,最好让程序挂掉,否则形成僵尸进程,让健康检查失效很严重。
十三. 构建可用模块(包)
注意点:
- 首字母大写代表包外代码可直接访问
- 代码的package可以和目录名不一致
- 同一目录的package要一致
- 下载第三方模块:
go get src ,强制更新:go get -u src
init函数 :在main函数执行之前,所有的init 都会按照包导入的顺序执行,每个包可以有多个init 函数,每个源文件也可以有多个init 函数
go的依赖问题:在新版本中可以使用go mod 来组织:在写完import 之后,在console中执行go mod tidy 即可自动整理下载包。
十四. Go的并发
14.1 协程机制
首先对比一下线程与协程:
- java thread stack 默认为1M
- Groutine的stack初始化为2K
- 和kernel space entity的对应关系:Java thread是1:1 Groutine是M:N
很明显可以看出协程更加轻量级,并且写成的切换不像java那样涉及到内核态,所以开销更小。
启动一个协程的方式:
func main() {
for i := 0; i < 10; i++ {
fmt.Println("main: ", i)
time.Sleep(time.Second * 1)
}
go func() {
for i := 0; i < 10; i++ {
fmt.Println("协程: ", i)
time.Sleep(time.Second * 1)
}
}()
time.Sleep(time.Second * 5)
}
14.2 GMP模型
- G是goroutine,基于协程建立的用户态线程
- M是machine,它直接关联一个os内核线程,用于执行G。
- P是processor,P里面一般会存当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度
在go中启动一个协程之后,会经历如下步骤:
14.3 共享内存并发机制
go语言中可以利用sync 包内的一些组件,例如:sync.Mutex, sync.WaitGroup
func main() {
sum := 0
for i := 0; i < 10000; i++ {
go func(i int) {
sum ++
}(i)
}
fmt.Println(sum)
}
func main() {
var mutex sync.Mutex
sum := 0
for i := 0; i < 10000; i++ {
go func(i int) {
defer func() {
mutex.Unlock()
}()
mutex.lock()
sum ++
}(i)
}
time.Sleep(time.Second)
fmt.Println(sum)
}
WaitGroup类似于java中的CountDownLatch,会有一个计数,然后每次Done一下,等数字Done到0了就放行。 上面那个例子中,我们在最后睡了一秒,但是这是非常不优雅的,可以修改如下:
func main() {
var mutex sync.Mutex
var wg sync.WaitGroup
sum := 0
for i := 0; i < 10000; i++ {
wg.Add(1)
go func(i int) {
defer func() {
mutex.Unlock()
}()
mutex.lock()
sum++
wg.Done()
}(i)
}
wg.Wait()
fmt.Println(sum)
}
14.4 CSP并发机制
交换消息的循序程序(communicating sequential processes),通过一个channel 来进行通讯,go的channel 是有容量的并且独立于协程。demo如下:
func taskOne() string {
time.Sleep(time.Second)
return "任务一的结果"
}
func taskTwo() {
fmt.Println("do something start")
time.Sleep(time.Second * 2)
fmt.Println("do something end")
}
func asyncTaskOne() chan string {
retCh := make(chan string)
go func() {
retCh <- taskOne()
}()
return retCh
}
func main() {
ch := asyncTaskOne()
taskTwo()
fmt.Println(<-ch)
}
注意点:如果channel的声明不给一个长度,那在没有人从channel中拿元素的时候,协程里面的那句话就会一直阻塞,可能导致协程泄露。改为如下写法即可:
go func() {
retCh := make(chan string, 1)
}
14.5 多路选择和超时控制
多路选择依靠关键字select 实现:
func service1() chan string {
retCh := make(chan string 1)
time.Sleep(time.Second * 1)
retCh <- "service1's result"
return retCh
}
func service2() chan string {
retCh := make(chan string 1)
time.Sleep(time.Second * 2)
retCh <- "service2's result"
return retCh
}
func main() {
select {
case ret1 := <-service1():
fmt.Println("先接受到了service1的返回")
case ret2 := <-service2():
fmt.Println("先接受到了service2的返回")
}
}
注意点:
- 如果同时有返回值,并不是按照定义顺序去判断的,可能是随机的
- 如果加了default,在当前select没有chan返回的情况下,就会执行default,而不是去等待某一个chan返回
超时控制,利用select没chan返回会阻塞的原理:
func service() chan string {
retCh := make(chan string 1)
time.Sleep(time.Second * 1)
retCh <- "service's result"
return retCh
}
func main() {
select {
case ret := <-service():
t.Log(ret)
case <-time.After(time.Millisecond * 100):
t.Error("time out")
}
}
14.6 channel的关闭
关闭一个channel的方法入下:
func main() {
ch := make(chan string, 1)
close(ch)
}
注意:
- 关闭
channel 之后再次向该通道发送数据将会触发panic 异常 - 关闭
channel 之后仍然可以正常取出剩下的元素,全部取完再取会得到零值
可以通过接受的第二个参数获取channel 是否仍然开启中:
ch := make(chan string, 1)
ch <- "hello"
if v, ok := <-ch; ok {
fmt.Println("channel未关闭")
} else {
fmt.Println("channel已关闭")
}
我们可以利用这个关闭机制做到广播:
func main() {
ch := make(chan int, 5)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
}()
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-ch)
}
}
}
上面的情况当我们知道具体需要生产多少个数据的时候是可以的,但是如果生产数据的个数是个未知数就无法操作了。或者有多个消费者也没办法,就需要依靠close广播,代码如下:
func main() {
ch := make(chan int, 5)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
go func() {
for {
if v, ok := <-ch; ok {
fmt.Println()
} else {
break
}
}
}
}
14.7 任务的取消
利用通道,和select语句,上面提到:当select中存在default语句时,如果第一时间没有值就会走default,我们可以利用这一点,判断是否其他协程发出了任务取消的消息。
func isCancelled(cancelChan chan struct{}) bool {
select {
case <-cancelChan:
return true
default:
return false
}
}
func cancel(cancelChan chan struct{}) {
cancelChan <- struct{}{}
}
func main() {
cancelChan := make(chan struct{})
go func(cancelChan chan struct{}) {
for{
if isCancelled(cancelChan) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(cancelChan)
time.Sleep(time.Second)
cancel(cancelChan)
}
但是上述代码有一个问题:如果有多个任务执行怎么办?难不成要发N个消息给channel吗?可以直接把channel关掉,这样它就不会走default了,会直接拿到零值。如下:
func cancel(cancelChan chan struct{}) {
close(cancelChan)
}
14.8 Context与任务取消
可以通过go语言提供的context来做超时,任务取消,任务截至时间等操作: 以任务取消为例改造14.7 的代码:
func isCancelled(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(cancelChan chan struct{}) {
for{
if isCancelled(ctx) {
break
}
time.Sleep(time.Millisecond * 5)
}
fmt.Println(i, "Cancelled")
}(cancelChan)
time.Sleep(time.Second)
cancel()
}
该代码仍可以完成同样的功能,有几点如下:
context.WithCancel() 需要的参数是一个parent,同样为context类型,代表这个环境是由哪个环境生成出来的,后续有用。如果自己就是根,那么就可以像上面那样写BackGround() 。- 当调用返回的
cancel() 函数时,所有由当前context 衍生出来的context 都会收到一个信号,即ctx.Done 能接到东西。
十五. 常见的并发任务(sync的使用)
15.1 多个协程只执行一次某个方法
以单例模式为例,java的代码是这样的(懒汉式的DCL写法):
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class) {
if(instance == null)
instance = new Singleton();
}
}
return instance;
}
}
很麻烦,又要私有化构造方法,还有用synchronized 关键字做双重锁检验,还要用volatile 防止指令重排序。如果用go的sync.Once 代码将如下:
var sync.Once
type Singleton struct {}
var singleInstance *Singleton
func getInstance() *Singleton {
once.Do(func() {
singleInstance = new(Singleton)
})
return singleInstance
}
这样就保证了只初始化一次,实现了单例模式。
15.2 仅需任意任务完成即可返回
在很多场景下是只需要多个执行路径下其中一条执行完毕就可以返回的,示例如下:
func getResponse(responseChannel chan string) string {
for i := 0; i < 10; i++ {
go func() {
response := doSomething()
responseChannel <- response
}()
}
return <-responseChannel
}
func main() {
responseChannel := make(chan string, 10)
firsteReponse := getResponse(responseChannel)
fmt.Println(firstReponse)
}
15.3 必须所有任务都完成
对上面的代码加以改造即可,将阻塞一次返回改成循环,阻塞N次返回。
func getResponse(responseChannel chan string) string {
for i := 0; i < 10; i++ {
go func() {
response := doSomething()
responseChannel <- response
}()
}
result := ""
for i := 0; i < 10; i++ {
result += <-responseChannel + "\n"
}
return result
}
func main() {
responseChannel := make(chan string, 10)
alleReponse := getResponse(responseChannel)
fmt.Println(alleReponse)
}
15.4 对象池
利用buffered channel 的空间存储对象:(这里不使用sync.Pool 存储)
type Obj struct {}
type ObjPool struct {
bufChan chan *Ob
}
func NewObjPool(objNums int) *ObjPool {
objPool := ObjPool{}
objPool.bufChan = make(chan *ReusableObj, objNums)
for i := 0; i < objNums; i++ {
objPool.bufChan <- &Obj{}
}
return &objPool
}
func (pool *ObjPool) GetObj(timeout time.Duration) (*Obj, error) (
select {
case obj := <-pool:
return obj, nil
case <-time.After(timeout):
return nil, errors.New("超时了")
}
}
func (pool *ObjPool) ReleaseObj(obj *Obj) error {
select {
case p.bufChan <- obj:
return nil
default:
return errors.New("overflow")
}
}
十六. 单元测试
16.1 go test
- 创建测试文件
xxx_test.go ,必须以_test.go 结尾 - 编写测试函数
TestXXX(t *testing.T) ,要以Test 开头,参数固定 - 执行命令
go test
示例如下:
func TestDemo(t *testing.T) {
fmt.Println("hello")
}
在该文件所在目录执行go test 结果如下: 也可以不使用fmt.Println 输出,使用t.Log 输出:
func TestDemo(t *testing.T) {
t.Log("hello")
}
此时执行go test 无法看到输出结果,如果要看到t 相关结果需要加参数-v ,即go test -v
t 参数还可以使用t.Error() 以及t.Fatal() ,二者都是代表测试失败(case不通,不是程序crash),区别如下: t.Error()``执行之后,该测试后续代码依然执行, t.Fatal()`之后后续代码将停止执行。
还可以指定某个测试函数运行,方法是加-run=regexp 参数,regexp是正则表达式,这里可以测试某个前缀的方法。例如想要测试TestCode(t *testing.T) ,那就可以这么写:go test -v -run=Code
-cover 参数可以测试代码覆盖率,就是该目录下哪些代码执行到了,哪些没有执行到(不包括_test.go 文件)
16.2 Benchmark
可以通过benchmark 来测试运行效率,使用方法:
- 将函数
TestXXX(t *testing.T) 改为BenchmarkXXX(b *testing.B) - 测试命令加上参数
go test -benchmark=. (这个点也是正则,现在代表所有的Benchmark函数,在windows中或者goland的terminal中注意加上双引号-benchmark="." b.resetTimer() 重置测试开始时间b.stopTimer() 停止测试时间b.startTimer() 开始测试时间- 一般通过345将要测试的代码片段包含起来,排除掉不想测的代码最后得到结果
func BenchmarkConcatStringByAdd(b *testing.B) {
elems := []string{"1", "2", "3", "4", "5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
ret := ""
for _, elem := range elems {
ret += elem
}
}
b.StopTimer()
}
func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
elems := []string{"1", "2", "3", "4", "5"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
for _, elem := range elems {
buf.WriteString(elem)
}
}
b.StopTimer()
}
也可以看到字符串直接+=是个很慢的操作
加上-benchmem 参数可以在上面的基础上看到每次操作用掉的内存大小。
十七. 反射
17.1 简介
反射可以通过字符串的方式取操作一个对象的属性和方法。先了解一下两个类型:
reflect.Type ,通过reflect.TypeOf(object) 获取reflect.Value ,通过reflect.ValueOf(object) 获取
我们可以通过上面两种类型的.Kind() 方法获取到object的种类,还可以通过.Name() 获取到它的类型,这两个是不一样的。类型是int, bool, type cat struct{}的cat 这些具体的类型。 种类是大的范围,包括基本类型和切片、通道、指针这些。
func CheckType(v interface{}) {
t := reflect.TypeOf(v)
switch t.Kind() {
case reflect.Float32, reflect.Float64:
fmt.Println("Float")
case reflect.Int, reflect.Int32, reflect.Int64:
fmt.Println("Integer")
default:
fmt.Println("Unknown", t)
}
}
func TestBasicType(t *testing.T) {
var number int = 12
CheckType(number )
}
17.2 通过反射修改值&&调用方法
可以通过反射编写更加灵活通用的代码:
type People struct {
Name string
Sex bool
}
func TestReflect(t *testing.T) {
p := &People{
Name: "jerry",
Sex: true,
}
val := reflect.ValueOf(p)
t.Log(val.Elem().FieldByName("Name"))
}
上面的注意点有两个:
Name 和Sex 属性必须首字母大写,否则根据包的知识它们是无法被reflect访问到的- 由于
.ValueOf() 传入的是个指针,所以我们如果想拿到它的属性要先去Elem() ,这相当于C中的* 操作
也可以调用方法:
func (p *People) Do() {
fmt.Println("wo......")
}
func TestReflect(t *testing.T) {
p := &People{
Name: "jerry",
Sex: true,
}
val := reflect.ValueOf(p)
val.MethodByName("Do").Call(nil)
}
注意点:有的方法在Type 和Value 中都有,有的是一样的作用,例如Kind() ,有的就是不一样的,例如Type 的FieldByName() 无法修改值,还是要加以区分。如果想要获取该对象的某些属性类型有关的,最好用Type ,和获取修改值有关的最好用Value
17.3 Struct Tag
结构体标记类似于java中的注解,反射中可以获取到这个tag
type People struct {
Name string `format:"name"`
Sex bool
}
这里可以用Type 类型获取到这个tag(只能用Type,它不属于值的概念)
func TestReflect(t *testing.T) {
p := &People{
Name: "jerry",
Sex: true,
}
ty := reflect.TypeOf(p)
name, _ := ty.Elem().FieldByName("Name")
t.Log(name.Tag.Get("format"))
}
17.4 万能程序
反射极大的提高了便利性,但是可读性和程序性能会降低。例如自带的json解析就是通过反射实现的,如果针对某个结构单独写一个json序列化,那么性能优于反射。
十八. 构建高可扩展的软件架构
18.1 Pipe-Filter
这种架构更像是线性的结构,通过一个pipe将多个过滤器串接在一起,当一个事件通过这个pipe的时候要把串起来的filter任务全都走一遍。
在实现上需要定义一个filter 接口,有输入流和输出流,让每个filter去实现它的process 方法。然后定义pipeline ,将需要的filter 都注册进去。一个大概的结构如下:
18.2 Micro-Kernel
微内核架构保持了一个核心,其他的功能都以可插拔的插件方式加入。这样的好处就是当其中一个功能(插件)崩掉之后,不会影响其他功能。并且非常容易拓展。
实现上需要以下几个关键点:
- 核心系统,主要用于注册插件和交互事件队列内容的存储:
Agent - 插件接口,用于实现功能:
Collector - 事件,如果没有事件,核心系统和各个插件无法交互:
Event
type Event struct {
Source string
Content string
}
type Collector interface {
Init(evtReceiver EventReceiver) error
Start(agtCtx context.Context) error
Stop() error
Destory() error
}
type Agent struct {
collectors map[string]Collector
evtBuf chan Event
cancel context.CancelFunc
ctx context.Context
state int
}
func (agt *Agent) RegisterCollector(name string, collector Collector) error {
if agt.state != Waiting {
return err
}
agt.collectors[name] = collector
err := collector.Init(collector)
return err
}
func (agt *Agent) startCollectors() error {
for _, collector := range agt.collectors {
err := collector.Start(agt)
}
}
func (agt *Agent) OnEvent(evt Event) {
agt.evtBuf <- evt
}
func (agt *Agent) EventProcessGroutine() {
var evtSeg [10]Event
for {
for i := 0; i < 10; i++ {
select {
case evtSeg[i] = <-agt.evtBuf:
case <-agt.ctx.Done():
return
}
}
fmt.Println(evtSeg)
}
}
type DemoCollector struct {
evtReceiver EventReceiver
agtCtx context.Context
stopChan chan struct{}
name string
content string
}
func (c *DemoCollector) Start(agtCtx context.Context) error {
fmt.Println("start collector", c.name)
for {
select {
case <-agtCtx.Done():
c.stopChan <- struct{}{}
break
default:
time.Sleep(time.Millisecond * 50)
c.evtReceiver.OnEvent(Event{c.name, c.content})
}
}
}
func (c *DemoCollector) Stop() error {
fmt.Println("stop collector", c.name)
select {
case <-c.stopChan:
return nil
case <-time.After(time.Second * 1):
return errors.New("failed to stop for timeout")
}
}
上面就是这个架构的核心实现了。
十九. 常见的一些任务
19.1 JSON解析
并不像java一样需要引入第三方库,go内置了json解析,通过反射实现: 核心方法就两个: 1.json.Marshal(obj) ,序列化,将对象序列化成字符串 2.json.Unmarshal([]byte(str). &Object) ,返回错误
使用方法如下:
type MyJson struct {
FirstField string `json:"first_field"`
SecondField string `json:"second_field"`
}
func TestJson(t *testing.T) {
myJsonObj := &MyJson{
FirstField: "第一个属性",
SecondField: "第二个属性",
}
if jsonBytes, err := json.Marshal(myJsonObj); err != nil {
fmt.Println("序列化错误")
} else {
fmt.Println(string(jsonBytes))
}
createObj := new(MyJson)
jsonStr := "{\"first_field\":\"第一个属性\",\"second_field\":\"第二个属性\"}"
if err := json.Unmarshal([]byte(jsonStr), createObj); err != nil {
fmt.Println("反序列化错误")
} else {
fmt.Println(createObj)
}
}
19.2 Easyjson(更快的序列化)
下载方式:go get -u github.com/mailru/easyjson/... 或者用go mod的情况下import然后执行 go mod tidy
在命令行中使用easyjson -all models.go ,models.go 是struct所在的文件,用法是对象.Marshal() ,同原生json
19.3 Http服务
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello World!"))
})
http.ListenAndServe(":8080", nil)
}
路由判断的规则:(上面写的规则)
- 不以
/ 结尾,是个固定路径,如果找不到就404了 - 以
/ 结尾,那么它能匹配任何子路径(前面一样后面不管什么样都可以匹配),当然一个URL被多个规则匹配到了就会找最长的。
后面可以用gin等框架,开发更方便
二十. 性能分析
20.1 性能文件查看
通过下列方法可以生成分析文件:
- CPU占用分析:
pprof.StartCPUProfile(file) 和 pprof.StopCPUProfile() (file可以用os.Create(filename) 生成 - 内存占用分析:
pprof.WriteHeapProfile(file) - 协程分析:
pprof.Lookup("goroutine")
运行结束后生成的文件用go tool pprof filename 即可
测试的同时也可以生成文件:go test -bench . -cpuprofile=cpu.prof 和 go test -bench . -memprofile=./mem.prof
性能调优可以通过以下步骤找到哪一部分耗时较多:
go test -bench=. -cpuprofile=cpu.prof ,利用b.N 去跑go tool pprof cpu.prof 生成的文件查看top 命令查看哪个耗时较多list functionName 查看具体哪一步消耗过大- 《经验调优》
- 吐槽:有些东西Mac的brew安装真的爽
20.2 sync.Map的锁问题
sync.Map的实现问题导致了它适用于读多写少的情况,一但读写差不多或者写多于读,那么sync.Map的性能甚至不如RWLock ,这时候建议用其他开源的并发map解决。
二十一. 写代码的建议
21.1 复杂对象传递引用
如结构和数组等,它们如果复制一份的开销是很大的
21.2 避免掉内存的分配和复制
map和slice当底层数组大小不够时,会扩容到二倍,这是会重新申请内存,并且复制一份。所以建议初始化到合适的大小
21.2 次数很多的字符串操作不要用+=
string是不可变对象,同样也不建议在多次操作fmt.SPrintf() 函数,建议使用strings.Builer ,版本过低建议使用bytes.Buffer ,用法相同如下:
func TestStringBuilder(t *testing.T) {
var builder strings.Builder
for i := 0; i < numbers; i++ {
builder.WriteString(strconv.Itoa(i))
}
result = builder.String()
}
基础入门完结
|