Go
——Life is short,Let’s go.
[toc]
参考 \ 推荐资料 :
[七米老师的视频]: https://www.bilibili.com/video/BV1fz4y1m7Pm?p=72&spm_id_from=pageDriver
[七米老师的博客]: https://www.liwenzhou.com/posts/Go/go_menu
Go官网:官网 国内镜像
Go安装包:官方安装包
Github:官方 中文版
Go语言标准库文档中文版
Go教程:官方英文版 官方中文版 菜鸟 go圣经 go四十二章经
Go论坛:国外(Go Forum StackOverflow Go ) 国内(Go语言中文网 golangtc )
Go在线编程:官方 菜鸟
输入
利用bufio.NewReader()
,来输入带空格的字符串
1
2
3
4
5
func main () {
reader := bufio . NewReader ( os . Stdin ) //os.Stdin是获取输入
s , _ := reader . ReadString ( '\n' ) //到'\n'为止
fmt . Println ( s )
}
三种指针
Go三种指针之间的关系
*类型:普通指针类型,用于传递对象地址,不能进行指针运算。
unsafe.Pointer:通用指针类型,用于转换不同类型的指针,不能进行指针运算,不能读取内存存储的值(必须转换到某一类型的普通指针)。
uintptr:用于指针运算,GC 不把 uintptr 当指针,uintptr 无法持有对象。uintptr 类型的目标会被回收。
unsafe.Pointer
unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:
(1)任何类型的指针都可以被转化为Pointer
(2)Pointer可以被转化为任何类型的指针
(3)uintptr可以被转化为Pointer
(4)Pointer可以被转化为uintptr
unsafe.Pointer是特别定义的一种指针类型(译注:类似C语言中的void类型的指针),在golang中是用于各种指针相互转换的桥梁,它可以包含任意类型变量的地址。
当然,我们不可以直接通过*p来获取unsafe.Pointer指针指向的真实变量的值,因为我们并不知道变量的具体类型。
和普通指针一样,unsafe.Pointer指针也是可以比较的,并且支持和nil常量比较判断是否为空指针。
Pointer是unsafe包中的万能指针,可以与任何类型的指针相互转换,但不可通过*p来直接读取其中的值(毕竟万能类型就等于没有类型嘛)。
uintptr
uintptr是一个整数类型。
1
2
// uintptr is an integer type that is large enough to hold the bit pattern of any pointer.
type uintptr uintptr
即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收,这个需要注意!。
*p
普通指针类型,用于传递对象地址,不能进行指针运算。
unsafe.Pointer与普通指针的转换
1
2
3
v := 0
fmt . Printf ( "%T" , unsafe . Pointer ( & v )) //unsafe.Pointer
fmt . Printf ( "%T" ,( * uint )( unsafe . Pointer ( & v ))) //*uint
unsafe包
是那个吧,就是那个吧,充满危险与支配感的那个unsafe
!
unsafe
包只有两个类型,三个函数。
1
2
3
4
5
type ArbitraryType int //int的一个别名,在Go中对ArbitraryType赋予特殊的意义。代表一个任意Go表达式类型。
type Pointer * ArbitraryType //int指针类型的一个别名,在Go中可以把Pointer类型,理解成任何指针的父类型。
func Sizeof ( x ArbitraryType ) uintptr //返回其占用的字节数
func Offsetof ( x ArbitraryType ) uintptr //返回结构体中元素所在内存的偏移量。
func Alignof ( x ArbitraryType ) uintptr //Alignof返回变量对齐字节数量Offsetof返回变量指定属性的偏移量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的属性当作参数。
内存对齐是,结构体中每个值按照内存占用最大的分配(不超过8,最大就是int64)。
偏移量就是内存中结构体里的变量所在的位置。
Go内存对齐
接口(interface)
接口:创造一种类型
如例2中只要具有speak()函数都可以是speaker类型,如果多个函数需要具有满足所有函数,要是没有任何函数interface{}
就等价于java中的Object
,c++中的any
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
type 接口名 interface {
函数 1 ()
函数 2 ()
......
}
//有多个函数的话需要同时具有这些函数才能满足接口
//不写里面函数叫空接口,所有类型都满足空接口
type speaker interface {}
type cat struct {
int
string
}
func geiwojiao ( x speaker ){
fmt . Println ( "x是:" , x )
}
/*
空接口没有起名的必要,所以可写作
func geiwojiao(x interface{}){}
*/
func main () {
a := cat {
1 ,
"猫" ,
}
geiwojiao ( a )
geiwojiao ( 1 )
}
//打印结果:x是:{1 猫}
// x是:1
1
2
3
4
//类型断言
var a interface {}
a = 100
i := a .( int ) //断言a为int从而得到int的返回值
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
package main
import (
"fmt"
)
type speaker interface {
speak ()
}
type cat struct {}
type dog struct {}
type monkey struct {}
func ( cat ) speak () {
fmt . Println ( "喵喵喵~" )
}
func ( dog ) speak () {
fmt . Println ( "妙啊~" )
}
func ( monkey ) speak () {
fmt . Println ( "giao giao" )
}
func geiwojiao ( x speaker ){
x . speak ()
}
func main () {
a := cat {}
b := dog {}
c := monkey {}
geiwojiao ( a )
geiwojiao ( b )
geiwojiao ( c )
}
空接口的类型判断(类型断言)
1
2
3
4
5
6
7
8
9
func hhh ( a interface {}){
fmt . Printf ( "%T" , a )
str , ok := a .( 猜测类型 ) //类型转换
if ! ok {
不是猜测类型
} else {
是猜测类型
}
}
或者
1
2
3
4
5
6
7
8
9
func hhh ( a interface {}){
fmt . Printf ( "%T" , a )
switch a .( type ){
case int :
case string :
case bool :
...
}
}
any(interface{})的类型判断
1
2
3
4
5
6
7
8
// 通过反射判断是否是struct类型
func isStruct ( i interface {}) bool {
return reflect . ValueOf ( i ). Type (). Kind () == reflect . Struct
}
// 同理 也可以判断是否是指针
func isPtr ( i interface {}) bool {
return reflect . ValueOf ( i ). Type (). Kind () == reflect . Ptr
}
go 1.18版本后
interface更新了一些新功能
不再只支持类型函数了,它可以支持正常的函数
1
2
3
4
5
interface {
Read ([] byte ) ( int , error )
Write ([] byte ) ( int , error )
Close () error
}
除了函数它也可以支持类型元素
1
2
3
4
// 它必须是int
interface {
int
}
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
// 它必须既是int又是string
// 没有能满足这一点的东西,但对于一些interface{}类型有用
// 比如既是interface{speak()}
interface {
int
string
}
// 实例
type speaker interface { speak ()}
type sleeper interface { sleep ()}
type T interface {
speaker
sleeper
}
type people struct {}
func ( p people ) speak () {
fmt . Println ( "汪汪汪" )
}
func ( p people ) sleep () {
fmt . Println ( "啊我睡了" )
}
func f1 ( t T ){
t . speak ()
t . sleep ()
}
func main () {
f1 ( people {})
}
1
2
// ~表示它的底层必须是uint8,byte是满足该接口的
interface { ~ uint8 }
1
2
// '|'表示满足任一即可,int,int64,rune等均可满足该接口
interface { int | int16 | ~ int32 | int64 | ~ uint8 }
==新关键字==
comparable
:支持==和!=的类型,数字\string\自定义结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// comparable测试
type s1 struct {
id int
}
func findFunc [ T comparable ]( a T , b T ) bool {
return a == b
}
func main () {
fmt . Println ( findFunc [ int ]( 3 , 2 )) //false
fmt . Println ( findFunc [ s1 ]( s1 { 0 }, s1 { 0 })) //true
//不支持切片:[]int does not implement comparable
//fmt.Println(findFunc[[]int]([]int{1},[]int{1}))
}
any
:interface{}的别名
==综合使用==
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
type myInt int
type myString string
func ( i myInt ) toString () string {
return strconv . Itoa ( int ( i ))
}
func ( s myString ) toString () string {
return string ( s )
}
type value interface {
toString () string
~ int | ~ string
}
func show [ T value ]( v T ){
fmt . Println ( v . toString ())
}
func main () {
var i myInt = 10
var str myString = "abc"
show ( i )
show ( str )
}
包(package)
包
import导入语句通常放在文件开头包声明语句的下面。
导入的包名需要使用双引号包裹起来。
包名是从$GOPATH/src/
后开始计算的,使用/
进行路径分隔。
Go语言中禁止循环导入包。
包的导入
1
2
import "包1" //导入包1
import 别名 "包2" //导入包2并起别名
匿名导入包
导入但不使用时可以,匿名导入,此时指触发包里的init()函数。
init()函数
在Go语言程序执行时导入包语句会自动触发包内部init()
函数的调用。需要注意的是: init()
函数没有参数也没有返回值。 init()
函数在程序运行时自动被调用执行,不能在代码中主动调用它。同一个包中init()
在全局变量定义之后执行。导入包时会直接执行该包的init()
,因此导入包的init()
最先执行。
1
2
3
4
5
6
7
8
var x int = 7
func init () {
fmt . Println ( x , y )
fmt . Println ( "main里的init" )
}
var y int = 100
在以上代码中x,y均可正常打印。
文件(file)
文件的打开、读、关闭
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
package main
import (
"fmt"
"io"
"os"
)
func main () {
Obj , err := os . Open ( "./foland.txt" ) //打开文件
defer Obj . Close () //defer在return后关闭文件
if err != nil {
fmt . Println ( "试过了,根本打不开。" )
return
}
tmp := make ([] byte , 256 ) //这里的256是切片长度也是一次读取的最大值
n , err := Obj . Read ( tmp ) //n是实际读入的字符数
if err == io . EOF { //EOF表示读到了末尾
fmt . Println ( "文件读完了" )
return
}
if err != nil {
fmt . Println ( "打开了,但是没有完全打开。" )
return
}
fmt . Printf ( "读取了%d字节数据\n" , n )
fmt . Println ( string ( tmp [: n ]))
}
读取文件存到一个[]byte
中,因此可以用循环来读
使用循环读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
tmp := make ([] byte , 256 )
var content [] byte
for {
n , err := Obj . Read ( tmp )
if err == io . EOF { //EOF表示读到了末尾
fmt . Println ( "文件读完了" )
return
}
if err != nil {
fmt . Println ( "打开了,但是没有完全打开。" )
return
}
conteny = append ( conteny , tmp [: n ]) //把读到的一段计入conteny数组
/*
if n < 256 { //也可以用n<tmp的长度来判断已读完
fmt.Println("文件读完了")
return
}
*/
}
利用bufio读取
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
package main
import (
"bufio"
"fmt"
"io"
"os"
)
func main () {
Obj , err := os . Open ( "./foland.txt" )
defer Obj . Close ()
if err != nil {
fmt . Println ( "试过了,根本打不开。" )
return
}
reader := bufio . NewReader ( Obj ) //reader是bufio中的结构体,包括[]byte数组,读、写位置,错误,尾字符
for {
line , err := reader . ReadString ( '\n' ) //line暂时储存直到读到'\n'之前的数据
if err == io . EOF {
if len ( line ) != 0 {
fmt . Println ( line )
}
fmt . Println ( "文件读完了" )
break
}
if err != nil {
fmt . Println ( "打开了,但是没有完全打开, err:" , err )
return
}
fmt . Print ( line )
}
}
利用ioutil读取整个文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"io/ioutil"
)
// ioutil.ReadFile读取整个文件
func main () {
content , err := ioutil . ReadFile ( "./main.go" ) //直接读完返回[]byte和err
if err != nil {
fmt . Println ( "read file failed, err:" , err )
return
}
fmt . Println ( string ( content ))
}
文件的写入
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
package main
import (
"bufio"
"fmt"
"os"
)
func main () {
file , err := os . OpenFile ( "./foland.txt" , os . O_CREATE | os . O_TRUNC | os . O_WRONLY , 0111 )
//第二个参数是打开模式,是int按2进制位运算,因此可以通过位运算融合
//第三个是给权限windows中这个没有意义
defer file . Close ()
if err != nil {
fmt . Println ( "笑死,根本找不到。" )
return
}
str := "妹红祈祷中~\n"
file . Write ([] byte ( str )) //写入[]byte类型的文本
str = "妹红折寿中~\n"
file . WriteString ( str ) //写入string类型的文本
writer := bufio . NewWriter ( file )
for i := 0 ; i < 10 ; i ++ {
writer . WriteString ( "QAQ\n" ) //将文本缓存在变量writer
}
writer . Flush () //缓存结束,写入
}
os.OpenFile第二个参数的含义
模式
含义
os.O_WRONLY
只写
os.O_CREATE
创建文件
os.O_RDONLY
只读
os.O_RDWR
读写
os.O_TRUNC
清空
os.O_APPEND
追加
利用ioutil.WriteFile写入
1
2
3
4
5
6
7
8
func main () {
str := "hello 沙河"
err := ioutil . WriteFile ( "./xx.txt" , [] byte ( str ), 0666 )
if err != nil {
fmt . Println ( "write file failed, err:" , err )
return
}
}
练习:拷贝文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"fmt"
"io/ioutil"
)
// ioutil.ReadFile读取整个文件
func main () {
content , err := ioutil . ReadFile ( "./main.go" ) //直接读完返回[]byte和err
if err != nil {
fmt . Println ( "read file failed, err:" , err )
return
}
err = ioutil . WriteFile ( "./foland.txt" , content , 0666 )
if err != nil {
fmt . Println ( "write file failed, err:" , err )
return
}
}
在中间插入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main
import (
"fmt"
"os"
)
func main () {
file , err := os . OpenFile ( "./foland.txt" , os . O_RDWR , 0666 )
if err != nil {
fmt . Println ( "打不开" )
return
}
defer file . Close ()
file . Seek ( 3 , 0 ) //移动光标
var a [ 18 ] byte
n , _ := file . Read ( a [:]) //复制光标后的全部字符
file . Seek ( 3 , 0 )
file . WriteString ( "QAQ" ) //写入想写的字符
file . Seek ( 6 , 0 )
file . Write ( a [: n ]) //把之前复制的字符再写回去
}
time包
当前时间,Now()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func timeDemo () {
now := time . Now () //获取当前时间
fmt . Printf ( "current time:%v\n" , now )
year := now . Year () //年
month := now . Month () //月
day := now . Day () //日
hour := now . Hour () //小时
minute := now . Minute () //分钟
second := now . Second () //秒
fmt . Printf ( "%d-%02d-%02d %02d:%02d:%02d\n" , year , month , day , hour , minute , second )
————————————————————————————————————————————————————————————————————————————————————————————————
fmt . Println ( time . Now (). Format ( "2006-01-02 15:04:05" )) //与上句等价,使用Format格式化输出。
}
定时器
time.Tick()
使用time.Tick(时间间隔)
来设置定时器,定时器的本质上是一个通道(channel)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func tickDemo () {
ticker := time . Tick ( 2 * time . Second ) //定义一个2秒间隔的定时器,传入一个time.Duration类型的变量
for i := range ticker {
fmt . Println ( i ) //每隔2秒都会执行
}
}
/*
time.Duration是time包定义的一个类型,它代表两个时间点之间经过的时间,以纳秒为单位。time.Duration表示一段时间间隔,可表示的最长时间段大约290年。
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
*/
time.Sleep()
1
2
3
func main () {
time . Sleep ( time . Second * 2 ) //输入等待的时间(Duration类型),如果输入数字会自己转,如果不是数字的int需要 time.Duration(a)函数类型转换
}
时间格式化
时间类型有一个自带的方法Format
进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S
而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)。
补充:如果想格式化为12小时方式,需指定PM
。
1
2
3
4
func main () {
n := time . Now ()
fmt . Println ( n . Format ( "2006-1-2 15:04:05:05.000" )) //格式规定,2006 1 2 3 4 5是goland的诞生时间
}
以上代码的打印效果为:当前时间的上述格式以做笔记时运行时间为例:2021-6-11 16:23:07:07.455
时间操作的几个函数
Add
我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求,Go语言的时间对象有提供Add方法如下:
1
func ( t Time ) Add ( d Duration ) Time
举个例子,求一个小时之后的时间:
1
2
3
4
5
func main () {
now := time . Now ()
later := now . Add ( time . Hour ) // 当前时间加1小时后的时间
fmt . Println ( later )
}
Sub
求两个时间之间的差值:
1
func ( t Time ) Sub ( u Time ) Duration
返回一个时间段t-u。如果结果超出了Duration可以表示的最大值/最小值,将返回最大值/最小值。要获取时间点t-d(d为Duration),可以使用t.Add(-d)。
Equal
1
func ( t Time ) Equal ( u Time ) bool
判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。本方法和用t==u不同,这种方法还会比较地点和时区信息。
Before
1
func ( t Time ) Before ( u Time ) bool
如果t代表的时间点在u之前,返回真;否则返回假。
After
1
func ( t Time ) After ( u Time ) bool
如果t代表的时间点在u之后,返回真;否则返回假。
时间戳
时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳。
把时间改为时间戳
1
2
3
4
5
func main () {
n := time . Now ()
fmt . Println ( n . Unix ()) //时间戳
fmt . Println ( n . UnixNano ()) //纳秒时间戳
}
把时间戳改为时间
1
2
3
4
func main () {
a := time . Unix ( 1623397977 , 0 )
fmt . Println ( a )
} //打印结果为:2021-06-11 15:52:57 +0800 CST
反射(reflect)
在程序运行期间打开、修改信息。
获取对象的类型:reflect.TypeOf()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main
import (
"fmt"
"reflect"
)
func reflectType ( x interface {}) {
v := reflect . TypeOf ( x )
fmt . Printf ( "type:%v\n" , v )
}
func main () {
var a float32 = 3.14
reflectType ( a ) // type:float32
var b int64 = 100
reflectType ( b ) // type:int64
}
name与kind
1
2
3
4
5
type book struct {}
x := book {}
//reflect.TypeOf(x).Name:book
//reflect.TypeOf(x).kind:struct
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
//kind中包含
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
获取对象的值:reflect.ValueOf()
reflect.ValueOf()
的返回值是reflect.Value
类型
1
2
3
var a int = 10
reflect . ValueOf ( a )= 10
reflect.Value
与原始值之间可以互相转换。
方法
说明
Interface() interface {}
将值以 interface{} 类型返回,可以通过类型断言转换为指定类型
Int() int64
将值以 int 类型返回,所有有符号整型均可以此方式返回
Uint() uint64
将值以 uint 类型返回,所有无符号整型均可以此方式返回
Float() float64
将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回
Bool() bool
将值以 bool 类型返回
Bytes() []bytes
将值以字节数组 []bytes 类型返回
String() string
将值以字符串类型返回
reflect.Value.Elem()访问指针的值
Elem()
是reflect.Value
类型的一个函数,作用是访问指针变量所指向的值,因为go语言的函数都是值拷贝,所以需要通过指针改变原来的变量。
结构体相关的方法
任意值通过reflect.TypeOf()
获得反射对象信息后,如果它的类型是结构体,可以通过反射值对象(reflect.Type
)的NumField()
和Field()
方法获得结构体成员的详细信息。
reflect.Type
中与获取结构体成员相关的的方法如下表所示。
方法
说明
Field(i int) StructField
根据索引,返回索引对应的结构体字段的信息。
NumField() int
返回结构体成员字段数量。
FieldByName(name string) (StructField, bool)
根据给定字符串返回字符串对应的结构体字段的信息。
FieldByIndex(index []int) StructField
多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息。
FieldByNameFunc(match func(string) bool) (StructField,bool)
根据传入的匹配函数匹配需要的字段。
NumMethod() int
返回该类型的方法集中方法的数目
Method(int) Method
返回该类型方法集中的第i个方法
MethodByName(string)(Method, bool)
根据方法名返回该类型方法集中的方法
ini配置文件
详见go项目:E:\实验\go源码\src\ini配置文件
数据类型转换:strconv包
strconv
包实现了基本数据类型和其字符串表示的相互转换。
1
2
3
4
5
6
7
8
9
func main () {
i := "2316"
j , _ := strconv . ParseInt ( i , 10 , 64 ) //将string转换为int64
fmt . Printf ( "%T %v\n" , j , j ) //打印结果:int64 2316
a := int64 ( 1234 )
b := strconv . FormatInt ( a , 10 ) //将int64转换为string
fmt . Printf ( "%T %v\n" , b , b ) //打印结果:string 1234
}
其他:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//string与int的快捷方法
func Atoi ( s string ) ( i int , err error ) //sting转int
func Itoa ( i int ) string //int转string
//string转其他类型
func ParseBool ( str string ) ( value bool , err error ) //转为bool,可输入1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,否则返回错误
func ParseInt ( s string , base int , bitSize int ) ( i int64 , err error ) //转为int64
func ParseUint ( s string , base int , bitSize int ) ( n uint64 , err error ) //转为uint64
func ParseFloat ( s string , bitSize int ) ( f float64 , err error ) //转为float64,如果超过flost64范围会四舍五入
//其他类型转sting
func FormatBool ( b bool ) string //bool转string,返回"true"or"false"
func FormatInt ( i int64 , base int ) string //int64转string
func FormatUint ( i uint64 , base int ) string //uint64转string
func FormatFloat ( f float64 , fmt byte , prec , bitSize int ) string //float64转string
//其他
func IsPrint ( r rune ) bool //判断一个字符是否可打印
func CanBackquote ( s string ) bool //返回字符串s是否可以不被修改的表示为一个单行的、没有空格和tab之外控制字符的反引号字符串。
//不止于此,详细自己ctrl进去看
并发
博客
并发是编程里面一个非常重要的概念,Go语言在语言层面天生支持并发,这也是Go语言流行的一个很重要的原因。
goroutine
使用goroutine
Go语言中使用goroutine
非常简单,只需要在调用函数的时候在前面加上go
关键字,就可以为一个函数创建一个goroutine
。
一个goroutine
必定对应一个函数,可以创建多个goroutine
去执行相同的函数。
程序启动时会创立一个主goroutine
——“main”
1
2
3
4
5
6
7
8
9
10
func hello (){
fmt . Println ( "Hello" )
}
func main () {
go hello () //创建一个goroutine去执行函数hello
fmt . Println ( "hi" )
//当main函数结束时所有由main开启的goroutine就都结束了,所以在终端中执行时hello来不及打印就结束了
//可以加一个time.Sleep(time.Millisecond)让程序稍微停一下
}
结束
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
24
25
26
27
28
//使用time.Sleep()函数来延时
func main () {
//go hello()//创建一个goroutine去执行函数hello
for i := 0 ; i < 10000 ; i ++ {
go func ( i int ){
fmt . Println ( "hi" , i )
}( i )
}
time . Sleep ( time . Second ) //停顿1s
}
---------------------------------------------------------------------------------------------------
//使用sync.WaitGroup来实现goroutine同步
var wg sync . WaitGroup
func hello ( i int ) {
defer wg . Done () //goroutine结束时登记-1
fmt . Println ( "Hello Goroutine!" , i )
}
func main () {
for i := 0 ; i < 10 ; i ++ {
wg . Add ( 1 ) //启动goroutine时登记+1
go hello ( i )
}
wg . Wait () //等待所有 登记==0 结束
}
goroutine与线程(os线程)
goroutine
是go语言自己的类似系统线程的东西,一个goroutine
的栈在刚创建时只有很小的栈(典型情况下2KB),goroutine
栈的大小可改变,(最大为1GB但很少用到这么大);而os线程(操作系统线程)一般都有固定的栈内存(一般为2MB),基于这个特点go语言可以轻松的一次创立100,000个goroutine
。
Go语言中的操作系统线程和goroutine
的关系:
一个操作系统线程对应用户态多个goroutine
。
go程序可以同时使用多个操作系统线程。
goroutine和OS线程是多对多的关系,(即下文m:n
)。
goroutine调度(理论)
GPM
是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。
G
很好理解,就是个goroutine
的,里面除了存放本goroutine
信息外 还有与所在P的绑定等信息。
P
管理着一组goroutine
队列,P里面会存储当前goroutine
运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine
暂停、运行后续的goroutine
等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
M(machine)
是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine
最终是要放到M上执行的;
P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。
P的个数是通过runtime.GOMAXPROCS
设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。
单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine
则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n
调度的技术(复用/调度m个goroutine到n个OS线程 )。 其一大特点是goroutine
的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine
均分在物理线程上, 再加上本身goroutine
的超轻量,以上种种保证了go调度方面的性能。
使用runtime.GOMAXPROCS()设置线程数
在多核心并发运行例子中两个函数时,应当乱序输出两个函数的,但貌似windows中不能正常运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main () {
var wg sync . WaitGroup
runtime . GOMAXPROCS ( 8 ) //设置跑程序时cpu的逻辑核心数(线程数),不写默认跑满
fmt . Println ( runtime . NumCPU ()) //打印电脑的逻辑核心数,我的是4核8线程的所有是8
wg . Add ( 2 )
go func (){
defer wg . Done ()
for i := 0 ; i < 10 ; i ++ {
fmt . Println ( "a:" , i )
}
}()
go func (){
defer wg . Done ()
for i := 0 ; i < 10 ; i ++ {
fmt . Println ( "b:" , i )
}
}()
wg . Wait ()
}
通道(channel)
channel
与slice
类似是引用类型,所以需要使用make分配内存才能使用。
1
2
3
4
5
6
var a chan int
fmt . Println ( a ) //<nil>
a = make ( chan int ) //非缓冲
fmt . Println ( a ) //分配之后就是一个地址
a = make ( chan int , 16 ) //缓冲16
fmt . Println ( a )
非缓冲 : make(chan int) channel
,channel
发送和接收动作是同时发生的,不能存数据,如果没有可以接收数据的goroutine就会直接阻塞
缓冲 : make(chan int,16) channel
类似一个队列,只有队列满了才可能发送阻塞
通道的操作
1.发送:ch <- 1
2.接收:x := <- ch
、<- ch
3.关闭:close()
,关闭后依旧可以从里面取值,但不可以写了
发送与接受
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
//无缓冲
func main () {
var a chan int
var wg sync . WaitGroup
wg . Add ( 1 )
a = make ( chan int )
defer close ( a )
go func (){
defer wg . Done ()
v :=<- a
fmt . Println ( v ) //也可以fmt.Println(<-a)
}()
a <- 10
wg . Wait ()
}
//------------------------------------------------------------------------------
//有缓冲
func main () {
var a chan int
a = make ( chan int , 1 ) //缓存为1
defer close ( a )
a <- 1
fmt . Println ( <- a ) //在这里已经把通道里的1取出来了,所以可以再放2
a <- 2
fmt . Println ( <- a )
a = make ( chan int , 2 )
a <- 3
a <- 4
fmt . Println ( <- a ) //打印结果:3
fmt . Println ( <- a ) //打印结果:4
}
当我们需要通过循环来从通道里取值的时候,不只是for
,range
也支持channel
close()
关闭通道后依旧可以取值,如果还有值取里面的值,ok的值是true,如果通道没有值了,返回对应类型的0值,ok的值是false。
1
2
3
4
5
6
7
8
9
10
11
12
func main () {
var a chan int
a = make ( chan int , 2 )
a <- 10
close ( a )
b , ok :=<- a
fmt . Println ( b , ok ) //打印10,true
b , ok = <- a
fmt . Println ( b , ok ) //打印0,false
b , ok = <- a
fmt . Println ( b , ok ) //打印0,false
}
小练习,100个数的平方
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 main () {
var wg sync . WaitGroup
wg . Add ( 2 )
var a chan int
var b chan int
a = make ( chan int )
b = make ( chan int )
defer close ( a )
go func (){
defer wg . Done ()
for i := 0 ; i < 100 ; i ++ {
a <- i
}
}()
go func (){
defer wg . Done ()
for i := 0 ; i < 100 ; i ++ {
j :=<- a
b <- j * j
}
}()
for i := 0 ; i < 100 ; i ++ {
fmt . Println ( <- b )
}
wg . Wait ()
}
单向通道
chan<-
和<-chan
自己写没必要使用单向通道,但单向通道能限制其他使用者的误操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var wg sync . WaitGroup
func f1 ( a chan <- int ){ //函数f1只允许将信息写入通道
defer wg . Done ()
a <- 10
}
func f2 ( a <- chan int ){ //函数f2只允许接收通道的信息
defer wg . Done ()
fmt . Println ( <- a )
}
func main () {
var a chan int
wg . Add ( 2 )
a = make ( chan int , 2 )
go f2 ( a )
go f1 ( a )
wg . Wait ()
}
通道总结
发送就是写入。
channel
常见的异常总结,如下图:
关闭已经关闭的channel
也会引发panic
。
worker pool(goroutine池)
在工作中我们通常会使用可以指定启动的goroutine数量–worker pool
模式,控制goroutine
的数量,防止goroutine
泄漏和暴涨。示例中为3个goroutine
处理5个任务。
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
func worker ( id int , jobs <- chan int , results chan <- int ) {
for j := range jobs {
fmt . Printf ( "worker:%d start job:%d\n" , id , j )
time . Sleep ( time . Second )
fmt . Printf ( "worker:%d end job:%d\n" , id , j )
results <- j
}
}
func main () {
jobs := make ( chan int , 100 )
results := make ( chan int , 100 )
// 开启3个goroutine
for w := 1 ; w <= 3 ; w ++ {
go worker ( w , jobs , results )
}
// 5个任务
for j := 1 ; j <= 5 ; j ++ {
jobs <- j
}
close ( jobs )
// 输出结果
for a := 1 ; a <= 5 ; a ++ {
<- results
}
}
worker pool的小练习
使用goroutine
和channel
实现一个计算int64随机数各位数和的程序。
开启一个goroutine
循环生成int64类型的随机数,发送到jobChan
开启24个goroutine
从jobChan
中取出随机数计算各位数的和,将结果发送到resultChan
主goroutine
从resultChan
取出结果并打印到终端输出
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
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var a = make ( chan * job , 50 )
var b = make ( chan * result , 50 )
type job struct {
x int
}
type result struct {
a * job
b int
}
func qiuhe ( a chan * job , b chan * result ){
defer wg . Done ()
for {
j :=<- a
sum := 0
n := j . x
for n > 0 {
sum += n % 10
n /= 10
}
// for j.x > 0 { 直接拿指针操作就会把结果变成0,TAT
// sum += j.x % 10
// j.x /= 10
// }
newresult := & result {
j ,
sum ,
}
b <- newresult
}
}
func suijishu ( a chan * job ) {
defer wg . Done ()
for { sui := rand . Int ()
a <-& job {
sui ,
}
time . Sleep ( time . Millisecond * 500 )
}
}
var wg sync . WaitGroup
func main () {
//for i:=0;i<50;i++{
wg . Add ( 1 )
go suijishu ( a )
//}
wg . Add ( 24 )
for i := 0 ; i < 24 ; i ++ {
go qiuhe ( a , b )}
for i := range b {
fmt . Println ( i . a . x , i . b )
}
wg . Wait ()
}
多路复用(Select)
可处理一个或多个channel的发送/接收操作。
如果多个case
同时满足,select
会随机选择一个。
对于没有case
的select{}
会一直等待,可用于阻塞main函数。
使用select
语句能提高代码的可读性。
select
的使用类似于switch语句,它有一系列case分支和一个默认的分支。每个case会对应一个通道的通信(接收或发送)过程。select
会一直等待,直到某个case
的通信操作完成时,就会执行case
分支对应的语句。具体格式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main () {
a := make ( chan int , 1 )
for i := 0 ; i < 10 ; i ++ {
select {
case x :=<- a :
fmt . Println ( x )
case a <- i :
}
}
time . Sleep ( time . Second )
//当a的缓冲区为1,不允许存入超过1个数,而没有数的时候无法读取,因此只有0,2,4,6,8
fmt . Println ()
a = make ( chan int , 10 )
for i := 0 ; i < 10 ; i ++ {
select {
case x :=<- a :
fmt . Println ( x )
case a <- i :
}
}
}
//a的缓冲区为10,除了0之外两个case都有可能满足,因此打印随机个随机数。
互斥锁
当两个goroutine
共同访问一个变量的时候,以下面代码为例,俩个goroutine
共同访问了x,两边都能拿到x,+1然后在放回去,但当两个goroutine
同时拿去x时就会少+1一次。
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
var x int
var wg sync . WaitGroup
func add (){
defer wg . Done ()
for i := 0 ; i < 50000 ; i ++ {
x += 1
}
}
func main () {
wg . Add ( 2 )
go add ()
go add ()
wg . Wait ()
fmt . Println ( x )
}
------------------------------- 使用互斥锁解决 -------------------------------
var x int
var lock sync . Mutex
var wg sync . WaitGroup
func add (){
defer wg . Done ()
for i := 0 ; i < 50000 ; i ++ {
lock . Lock ()
x += 1
lock . Unlock ()
}
}
func main () {
wg . Add ( 2 )
go add ()
go add ()
wg . Wait ()
fmt . Println ( x )
}
原理大概是当一个端口运行到lock.Lock()
时会判断lock是不是锁着的,如果是锁着的就等解锁再运行。顺便如果所有端口都没有解锁就会堵塞:all goroutines are asleep - deadlock!
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine
可以访问共享资源。Go语言中使用sync
包的Mutex
类型来实现互斥锁。
ps:多个goroutine
同时等待一个锁时,唤醒的策略是随机的。
读、写互斥锁
区分读与写的锁,避免写的锁耽误了读,一般运用于读取的频率远大于写入频率时。
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
package main
import (
"fmt"
"sync"
"time"
)
var x int
var lock sync . Mutex
var wg sync . WaitGroup
var rwlock sync . RWMutex
func read () {
defer wg . Done ()
rwlock . RLock ()
//fmt.Println(x)
time . Sleep ( time . Millisecond )
rwlock . RUnlock ()
}
func write () {
defer wg . Done ()
rwlock . Lock ()
x = x + 1
time . Sleep ( time . Millisecond * 10 )
rwlock . Unlock ()
}
func main () {
start := time . Now ()
for i := 0 ; i < 10 ; i ++ {
wg . Add ( 1 )
go write ()
}
for i := 0 ; i < 1000 ; i ++ {
wg . Add ( 1 )
go read ()
}
wg . Wait ()
fmt . Println ( x , time . Now (). Sub ( start ))
}
sync.Once
1
func ( o * Once ) Do ( f func ()) {}
确保函数执行一次。
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
var once sync . Once
func f1 ( a chan int ) {
for i := 0 ; i < 10 ; i ++ {
a <- i
}
close ( a )
}
func f2 ( a , b chan int ) {
for {
x , ok :=<- a
if ! ok {
break
}
b <- x * x
}
once . Do ( func () {
close ( b )
})
}
func main () {
a := make ( chan int , 1 )
b := make ( chan int , 1 )
go f1 ( a )
go f2 ( a , b )
go f2 ( a , b )
time . Sleep ( time . Microsecond * 1000 )
for i := range b {
fmt . Println ( i )
}
}
sync.Once
其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
sync.Map
Go语言中内置的map不是并发安全的。当赋值与创立被多个goroutine
运行时可能会出错因此可以使用syne.Map
来实现并发安全的Map。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var m = sync . Map {}
func main () {
wg := sync . WaitGroup {}
for i := 0 ; i < 20 ; i ++ {
wg . Add ( 1 )
go func ( n int ) {
key := strconv . Itoa ( n )
m . Store ( key , n )
value , _ := m . Load ( key )
fmt . Printf ( "k=:%v,v:=%v\n" , key , value )
wg . Done ()
}( i )
}
wg . Wait ()
}
Go语言的sync
包中提供了一个开箱即用的并发安全版map–sync.Map
。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map
内置了诸如Store
、Load
、LoadOrStore
、Delete
、Range
等操作方法。
原子操作(atomic包)
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
---------------------------------------------- 读取 -----------------------------------------------------------
func LoadInt32 ( addr * int32 ) ( val int32 ) //并发安全的拿到addr的值
func LoadInt64 ( addr * int64 ) ( val int64 ) //例如:v:=atomic.LoadInt32(&addr)
func LoadUint32 ( addr * uint32 ) ( val uint32 )
func LoadUint64 ( addr * uint64 ) ( val uint64 )
func LoadUintptr ( addr * uintptr ) ( val uintptr )
func LoadPointer ( addr * unsafe . Pointer ) ( val unsafe . Pointer )
---------------------------------------------- 写入 -----------------------------------------------------------
func StoreInt32 ( addr * int32 , val int32 ) //并发安全的把addr赋值为val
func StoreInt64 ( addr * int64 , val int64 )
func StoreUint32 ( addr * uint32 , val uint32 )
func StoreUint64 ( addr * uint64 , val uint64 )
func StoreUintptr ( addr * uintptr , val uintptr )
func StorePointer ( addr * unsafe . Pointer , val unsafe . Pointer )
---------------------------------------------- 修改 -----------------------------------------------------------
func AddInt32 ( addr * int32 , delta int32 ) ( new int32 ) //使addr+=delta,需要减时可以令delta为负数
func AddInt64 ( addr * int64 , delta int64 ) ( new int64 )
func AddUint32 ( addr * uint32 , delta uint32 ) ( new uint32 )
func AddUint64 ( addr * uint64 , delta uint64 ) ( new uint64 )
func AddUintptr ( addr * uintptr , delta uintptr ) ( new uintptr )
---------------------------------------------- 交换 -----------------------------------------------------------
func SwapInt32 ( addr * int32 , new int32 ) ( old int32 ) //使addr=new,返回addr赋值前的值
func SwapInt64 ( addr * int64 , new int64 ) ( old int64 )
func SwapUint32 ( addr * uint32 , new uint32 ) ( old uint32 ) //v1:=int32(1),v2:=atomic.SwapInt32(&v,2)
func SwapUint64 ( addr * uint64 , new uint64 ) ( old uint64 ) //fmt.Println(v1,v2) 结果为2,1
func SwapUintptr ( addr * uintptr , new uintptr ) ( old uintptr )
func SwapPointer ( addr * unsafe . Pointer , new unsafe . Pointer ) ( old unsafe . Pointer )
-------------------------------------------- 比较并交换 --------------------------------------------------------
func CompareAndSwapInt32 ( addr * int32 , old , new int32 ) ( swapped bool ) //如果addr和old相同,就用new代替addr,返回true
func CompareAndSwapInt64 ( addr * int64 , old , new int64 ) ( swapped bool ) //如果不相等就返回false
func CompareAndSwapUint32 ( addr * uint32 , old , new uint32 ) ( swapped bool )
func CompareAndSwapUint64 ( addr * uint64 , old , new uint64 ) ( swapped bool )
func CompareAndSwapUintptr ( addr * uintptr , old , new uintptr ) ( swapped bool )
func CompareAndSwapPointer ( addr * unsafe . Pointer , old , new unsafe . Pointer ) ( swapped bool )
网络编程(socket编程)
七层模型
关于网络编程的李文周老师的博客
Socket编程
Socket
是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket
后面,对用户来说只需要调用Socket规定的相关函数,让Socket
去组织符合指定的协议数据然后进行通信。
Socket
把编写程序的应用层与其他层分割了,因此Socket
编程只需要调用函数去操作Socket
层就好了。
TCP通信
TCP通信的”三次握手,四次挥手“
握手 :确保服务器与客户端收发功能正常,指定自己的初始化序列号,为后面的可靠传送做准备,如果是 https 协议的话,三次握手这个过程,还会进行数字证书的验证以及加密密钥的生成到。
1、第一次握手:客户端给服务器发送一个 SYN 报文。
2、第二次握手:服务器收到 SYN 报文之后,会应答一个 SYN+ACK 报文。
3、第三次握手:客户端收到 SYN+ACK 报文之后,会回应一个 ACK 报文。
4、服务器收到 ACK 报文之后,三次握手建立完成。
挥手 :保证服务器知道客户端关闭了。
1、第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于FIN_WAIT1状态。
2、第二次握手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态。
3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态
5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
服务端
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
// tcp/server/main.go
// TCP server端
// 处理函数
func process ( conn net . Conn ) {
defer conn . Close () // 关闭连接
for {
reader := bufio . NewReader ( conn )
var buf [ 128 ] byte
n , err := reader . Read ( buf [:]) // 读取数据
if err != nil {
fmt . Println ( "read from client failed, err:" , err )
break
}
recvStr := string ( buf [: n ])
fmt . Println ( "收到client端发来的数据:" , recvStr )
conn . Write ([] byte ( recvStr )) // 发送数据
}
}
func main () {
listen , err := net . Listen ( "tcp" , "127.0.0.1:20000" )
if err != nil {
fmt . Println ( "listen failed, err:" , err )
return
}
for {
conn , err := listen . Accept () // 建立连接
if err != nil {
fmt . Println ( "accept failed, err:" , err )
continue
}
go process ( conn ) // 启动一个goroutine处理连接
}
}
客户端
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
// tcp/client/main.go
// 客户端
func main () {
conn , err := net . Dial ( "tcp" , "127.0.0.1:20000" )
if err != nil {
fmt . Println ( "err :" , err )
return
}
defer conn . Close () // 关闭连接
inputReader := bufio . NewReader ( os . Stdin )
for {
input , _ := inputReader . ReadString ( '\n' ) // 读取用户输入
inputInfo := strings . Trim ( input , "\r\n" )
if strings . ToUpper ( inputInfo ) == "Q" { // 如果输入q就退出
return
}
_ , err = conn . Write ([] byte ( inputInfo )) // 发送数据
if err != nil {
return
}
buf := [ 512 ] byte {}
n , err := conn . Read ( buf [:])
if err != nil {
fmt . Println ( "recv failed, err:" , err )
return
}
fmt . Println ( string ( buf [: n ]))
}
}
粘包
黏包示例
服务端代码如下:
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
// socket_stick/server/main.go
func process ( conn net . Conn ) {
defer conn . Close ()
reader := bufio . NewReader ( conn )
var buf [ 1024 ] byte
for {
n , err := reader . Read ( buf [:])
if err == io . EOF {
break
}
if err != nil {
fmt . Println ( "read from client failed, err:" , err )
break
}
recvStr := string ( buf [: n ])
fmt . Println ( "收到client发来的数据:" , recvStr )
}
}
func main () {
listen , err := net . Listen ( "tcp" , "127.0.0.1:30000" )
if err != nil {
fmt . Println ( "listen failed, err:" , err )
return
}
defer listen . Close ()
for {
conn , err := listen . Accept ()
if err != nil {
fmt . Println ( "accept failed, err:" , err )
continue
}
go process ( conn )
}
}
客户端代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// socket_stick/client/main.go
func main () {
conn , err := net . Dial ( "tcp" , "127.0.0.1:30000" )
if err != nil {
fmt . Println ( "dial failed, err" , err )
return
}
defer conn . Close ()
for i := 0 ; i < 20 ; i ++ {
msg := `Hello, Hello. How are you?`
conn . Write ([] byte ( msg ))
}
}
将上面的代码保存后,分别编译。先启动服务端再启动客户端,可以看到服务端输出结果如下:
1
2
3
4
5
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?
收到client发来的数据: Hello, Hello. How are you?Hello, Hello. How are you?
客户端分10次发送的数据,在服务端并没有成功的输出10次,而是多条数据“粘”到了一起。
“粘包”可发生在发送端也可发生在接收端:
由Nagle算法造成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时,TCP并不立刻发送此段数据,而是等待一小段时间看看在等待期间是否还有要发送的数据,若有则会一次把这两段数据发送出去。
接收端接收不及时造成的接收端粘包:TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来,就会造成TCP缓冲区中存放了几段数据。
解决
自己定义一个协议,自定义一个编码解码的方法,比如数据包的前4个字节为包头,里面存储的是发送的数据的长度。
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
// socket_stick/proto/proto.go
package proto
import (
"bufio"
"bytes"
"encoding/binary" //binary包实现了简单的数字与字节序列的转换以及变长值的编解码,
)
// Encode 将消息编码
func Encode ( message string ) ([] byte , error ) {
// 读取消息的长度,转换成int32类型(占4个字节)
var length = int32 ( len ( message ))
var pkg = new ( bytes . Buffer )
// 写入消息长度,
//LittleEndian是编码方式小端,从后向前写。
err := binary . Write ( pkg , binary . LittleEndian , length )
if err != nil {
return nil , err
}
// 写入消息实体
err = binary . Write ( pkg , binary . LittleEndian , [] byte ( message ))
if err != nil {
return nil , err
}
return pkg . Bytes (), nil
}
// Decode 解码
func Decode ( reader * bufio . Reader ) ( string , error ) {
// 读取消息的长度
lengthByte , _ := reader . Peek ( 4 ) // 读取前4个字节的数据
lengthBuff := bytes . NewBuffer ( lengthByte )
var length int32
err := binary . Read ( lengthBuff , binary . LittleEndian , & length ) //解码时保证与编码方式一致
if err != nil {
return "" , err
}
if int32 ( reader . Buffered ()) < length + 4 { //Buffered返回缓冲中现有的可读取的字节数。
return "" , err
}
// 读取真正的消息数据
pack := make ([] byte , int ( 4 + length ))
_ , err = reader . Read ( pack )
if err != nil {
return "" , err
}
return string ( pack [ 4 :]), nil
}
接下来在服务端和客户端分别使用上面定义的proto
包的Decode
和Encode
函数处理数据。
服务端代码如下:
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
// socket_stick/server2/main.go
func process ( conn net . Conn ) {
defer conn . Close ()
reader := bufio . NewReader ( conn )
for {
msg , err := proto . Decode ( reader )
if err == io . EOF {
return
}
if err != nil {
fmt . Println ( "decode msg failed, err:" , err )
return
}
fmt . Println ( "收到client发来的数据:" , msg )
}
}
func main () {
listen , err := net . Listen ( "tcp" , "127.0.0.1:30000" )
if err != nil {
fmt . Println ( "listen failed, err:" , err )
return
}
defer listen . Close ()
for {
conn , err := listen . Accept ()
if err != nil {
fmt . Println ( "accept failed, err:" , err )
continue
}
go process ( conn )
}
}
客户端代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// socket_stick/client2/main.go
func main () {
conn , err := net . Dial ( "tcp" , "127.0.0.1:30000" )
if err != nil {
fmt . Println ( "dial failed, err" , err )
return
}
defer conn . Close ()
for i := 0 ; i < 20 ; i ++ {
msg := `Hello, Hello. How are you?`
data , err := proto . Encode ( msg )
if err != nil {
fmt . Println ( "encode msg failed, err:" , err )
return
}
conn . Write ( data )
}
}
UDP通信
UDP通信是一种无连接 的传输层协议,不需要建立连接就能直接进行数据发送和接收,属于不可靠的、没有时序的通信,但是UDP协议的实时性比较好,通常用于视频直播相关领域。
UDP服务端
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
func main () {
listener , err := net . ListenUDP ( "udp" , & net . UDPAddr {
IP : net . IPv4 ( 127 , 0 , 0 , 1 ), //net.UDPAddr结构体包括:IP:ip地址,Port:端口,Zone:IPv6的寻址路径
Port : 20000 ,
})
if err != nil {
fmt . Println ( "error:" , err )
return
}
defer listener . Close ()
var date [ 1024 ] byte
for {
n , addr , err := listener . ReadFromUDP ( date [:])
if err != nil {
fmt . Println ( "error:" , err )
continue
}
fmt . Println ( string ( date [: n ]), addr , n )
_ , err = listener . WriteToUDP ( date [: n ], addr )
if err != nil {
fmt . Println ( "error:" , err )
continue
}
}
}
UDP客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main () {
dial , err := net . DialUDP ( "udp" , nil , & net . UDPAddr {
IP : net . IPv4 ( 127 , 0 , 0 , 1 ),
Port : 20000 ,
})
if err != nil {
fmt . Println ( "连接失败" , err )
return
}
defer dial . Close ()
reader := bufio . NewReader ( os . Stdin )
reply := make ([] byte , 1024 )
for {
msg , _ := reader . ReadString ( '\n' )
dial . Write ([] byte ( msg ))
n , _ , err := dial . ReadFromUDP ( reply )
if err != nil {
fmt . Println ( "没收到信息" )
return
}
fmt . Println ( string ( reply [: n ]), "发出去了" )
}
}
Go测试
程序员自己测试函数的文件。主要分为三种:
类型
格式
作用
测试函数
函数名前缀为Test
测试程序的一些逻辑行为是否正确
基准函数
函数名前缀为Benchmark
测试函数的性能
示例函数
函数名前缀为Example
为文档提供示例文档
单元测试
以分割字符的函数Split
为例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package split_string
import "strings"
func Split ( str string , key string )[] string {
var ans [] string
a := strings . Index ( str , key )
for a >= 0 {
if string ([] byte ( str )[: a ]) != "" {
ans = append ( ans , string ([] byte ( str )[: a ]))}
str = string ([] byte ( str )[ a + len ( key ):])
a = strings . Index ( str , key )
}
if str != "" {
ans = append ( ans , str )}
return ans
}
Split
函数不在可执行的main
包中,可以在同一文件夹下(此处为split_string文件夹)新建一个以_test
结尾的测试文件。
像这样。
测试文件代码:
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
package split_string
import (
"reflect"
"testing"
)
func Test1Splitt ( t * testing . T ) {
got := Split ( "a:b:c" , ":" )
want := [] string { "a" , "b" , "c" }
if ! reflect . DeepEqual ( got , want ){
t . Errorf ( "want:%#v,but got=%#v\n" , want , got )
}
}
func Test2Split ( t * testing . T ) {
got := Split ( "ddddd" , "d" )
var want [] string
want = nil
if ! reflect . DeepEqual ( got , want ){
t . Errorf ( "want:%#v,but got=%#v\n" , want , got )
}
}
func Test3Split ( t * testing . T ) {
got := Split ( "" , "d" )
var want [] string
want = nil
if ! reflect . DeepEqual ( got , want ){
t . Errorf ( "want:%#v,but got=%#v\n" , want , got )
}
}
func TestSplit(t *testing.T){}
就相当于主函数,在split_string
包路径下使用go test
运行测试文件,也可以为go test
命令添加-v
参数,查看测试函数名称和运行时间,还可以在go test
命令后添加-run
参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test
命令执行。
测试组
可以看到上面的单元测试代码的几个函数是重复的、麻烦的,因此可以把测试用例做成切片堆到一个函数里,就是测试组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestSplit ( t * testing . T ) {
type testCase struct {
str string
key string
want [] string
}
testGroup := [] testCase {
{ "a:b:c" , ":" ,[] string { "a" , "b" , "c" }},
{ "ddddd" , "d" , nil },
{ "" , "d" , nil },
}
for j , i := range testGroup {
if ! reflect . DeepEqual ( Split ( i . str , i . key ), i . want ){
t . Errorf ( "test%d:want:%#v,but got=%#v\n" , j , i . want , Split ( i . str , i . key ))
}
}
}
子测试
与测试组类似,不是将测试用例做成切片,而是做成map,将Error
语句包在 t.Run(j, func(t *testing.T) {})
里,这样做的好处是当测试用例出错时,它可以报告是哪个测试用例出错了,当然像下面这样直接把test的序号打印出来实际上也是一样的效果。
还有一个作用是可以使用go test -run=
指定需要测试的某个用例,如下面的实例中,执行go test -run=TestSplit/case3
,就是只测试case3。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func TestSplit ( t * testing . T ) {
type testCase struct {
str string
key string
want [] string
}
testGroup := map [ string ] testCase {
"case1" : testCase { "a:b:c" , ":" ,[] string { "a" , "b" , "c" }},
"case2" : testCase { "ddddd" , "d" , nil },
"case3" : testCase { "" , "d" , nil },
}
for j , i := range testGroup {
t . Run ( j , func ( t * testing . T ) {
if ! reflect . DeepEqual ( Split ( i . str , i . key ), i . want ){
t . Errorf ( "test%v:want:%#v,but got=%#v\n" , j , i . want , Split ( i . str , i . key ))
}
})
}
}
测试覆盖率
公司一般会有要求测试覆盖率(代码有多少语句被测试过),可以使用 go test -cover
查看。
Go还提供了一个额外的-coverprofile
参数,用来将覆盖率相关的记录信息输出到一个任意命名的文件。
例如:go test -cover -coverprofile=c.out
创建一个c.out
文件存放测试覆盖率相关信息,然后我们执行go tool cover -html=c.out
,使用cover
工具来处理生成的记录信息,该命令会打开本地的浏览器窗口生成一个网页报告,绿色表示覆盖到,红色是没覆盖到。
基准测试
与单元测试类似,可以写在同一个文件夹里,前缀换成Benchmark
,b.N是一个次数,for i:=0;i<b.N;i++{}
算是一个格式。运行时输入go test -bench=测试函数名
此实例为go test -bench=Split
。
1
2
3
4
5
func BenchmarkSplit ( b * testing . B ) {
for i := 0 ; i < b . N ; i ++ {
Split ( "a:b:c" , ":" )
}
}
运行结果:
1
2
3
4
5
6
7
8
goos: windows //windows平台
goarch: amd64 //amd64位
pkg: 单元测试/split_string //包
cpu: Intel( R) Core( TM) i5-9300H CPU @ 2.40GHz //cpu
BenchmarkSplit-8 //8线程 4988179 //执行了498179次 239.3 ns/op //平均239.3纳秒每次
PASS
ok 单元测试/split_string 1.616s
也可以使用split_string>go test -bench=函数名 -benchmem
查看空间信息,如果上述函数名处输入’.‘则运行所有bench函数。
运行结果:
1
2
3
4
5
6
7
goos: windows
goarch: amd64
pkg: 单元测试/split_string
cpu: Intel( R) Core( TM) i5-9300H CPU @ 2.40GHz
BenchmarkSplit-8 4994647 237.9 ns/op 115 B/op 4 allocs/op
PASS
ok 单元测试/split_string 1.461s
115 B/op
表示每次操作内存分配了112字节,4 allocs/op
则表示每次操作进行了4次内存分配。
因为函数使用了append
动态分配内存,所以内存分配次数和运行时间长,可以在一开始就分配一个足够大的内存,这样可以快很多。
性能比较函数
通过一定时间(默认为1s)内对于不同用例,函数能运行的次数,来比较该函数的性能优劣。
已Fib斐波那契数列的函数为例。
1
2
3
4
5
6
7
8
package fib
func Fib ( n int ) int {
if n < 2 {
return n
}
return Fib ( n - 1 ) + Fib ( n - 2 )
}
测试函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package fib
import "testing"
func benchmarkFib2 ( b * testing . B , n int ) {
for i := 0 ; i < b . N ; i ++ {
Fib ( n )
}
}
func BenchmarkFib1 ( b * testing . B ) { benchmarkFib2 ( b , 1 )}
func BenchmarkFib2 ( b * testing . B ) { benchmarkFib2 ( b , 2 )}
func BenchmarkFib3 ( b * testing . B ) { benchmarkFib2 ( b , 3 )}
func BenchmarkFib10 ( b * testing . B ) { benchmarkFib2 ( b , 10 )}
func BenchmarkFib20 ( b * testing . B ) { benchmarkFib2 ( b , 20 )}
func BenchmarkFib40 ( b * testing . B ) { benchmarkFib2 ( b , 40 )}
输入go test -bench=.
运行所有bench,运行运行结果:
1
2
3
4
5
6
7
8
9
10
11
12
13
E:\实 验\G o源码\s rc\单 元测试\f ib>go test -bench= .
goos: windows
goarch: amd64
pkg: 单元测试/fib
cpu: Intel( R) Core( TM) i5-9300H CPU @ 2.40GHz
BenchmarkFib1-8 455233650 2.635 ns/op
BenchmarkFib2-8 142676842 8.439 ns/op
BenchmarkFib3-8 85940185 13.12 ns/op
BenchmarkFib10-8 2377339 503.5 ns/op
BenchmarkFib20-8 19413 62032 ns/op
BenchmarkFib40-8 2 936843000 ns/op
PASS
ok 单元测试/fib 11.312s
可以看到一秒内Fib40只能运行2次,为了追求准确可以使用benchtime延长运行的时间,如go test -bench=Fib40 -benchtime=20s
重置时间
b.ResetTimer
之前的处理不会放到执行时间里,也不会输出到报告中,所以可以在之前做一些不计划作为测试报告的操作。例如:
1
2
3
4
5
6
7
func BenchmarkSplit ( b * testing . B ) {
time . Sleep ( 5 * time . Second ) // 假设需要做一些耗时的无关操作
b . ResetTimer () // 重置计时器
for i := 0 ; i < b . N ; i ++ {
Split ( "沙河有沙又有河" , "沙" )
}
}
并行测试
func (b *B) RunParallel(body func(*PB))
会以并行的方式执行给定的基准测试。
RunParallel
会创建出多个goroutine
,并将b.N
分配给这些goroutine
执行, 其中goroutine
数量的默认值为GOMAXPROCS
。用户如果想要增加非CPU受限(non-CPU-bound)基准测试的并行性, 那么可以在RunParallel
之前调用SetParallelism
。RunParallel
通常会与-cpu
标志一同使用。
1
2
3
4
5
6
7
8
func BenchmarkSplitParallel ( b * testing . B ) {
// b.SetParallelism(1) // 设置使用的CPU数
b . RunParallel ( func ( pb * testing . PB ) {
for pb . Next () {
Split ( "沙河有沙又有河" , "沙" )
}
})
}
执行一下基准测试:
1
2
3
4
5
6
7
8
split $ go test -bench= .
goos: darwin
goarch: amd64
pkg: github.com/Q1mi/studygo/code_demo/test_demo/split
BenchmarkSplit-8 10000000 131 ns/op
BenchmarkSplitParallel-8 50000000 36.1 ns/op
PASS
ok github.com/Q1mi/studygo/code_demo/test_demo/split 3.308s
还可以通过在测试命令后添加-cpu
参数如go test -bench=. -cpu 1
来指定使用的CPU数量。(这个实例没有开goroutine所以结果相差不大)
Setup与TearDown
测试程序有时需要在测试之前进行额外的设置(setup)或在测试之后进行还原(teardown)。
框架
1
2
3
4
5
6
7
func TestMain ( m * testing . M ) {
fmt . Println ( "write setup code here..." ) // 测试之前的做一些设置
// 如果 TestMain 使用了 flags,这里应该加上flag.Parse()
retCode := m . Run () // 具体执行的测试
fmt . Println ( "write teardown code here..." ) // 测试之后做一些拆卸工作
os . Exit ( retCode ) // 退出测试
}
示例函数
示例函数以Example
为前缀。示例为:
1
2
3
4
5
6
7
func ExampleSplit () {
fmt . Println ( split . Split ( "a:b:c" , ":" ))
fmt . Println ( split . Split ( "沙河有沙又有河" , "沙" ))
// Output:
// [a b c]
// [ 河有 又有河]
}
其中// Output:
是可以通过go test -run Example
执行,Output:
下面写的就相当于单元测试里的want。
Go性能调优
在计算机性能调试领域里,profiling 是指对应用程序的画像,画像就是应用程序使用 CPU 和内存的情况。
Go语言项目中的性能优化主要有以下几个方面:
CPU profile:报告程序的 CPU 使用情况,按照一定频率去采集应用程序在 CPU 和寄存器上面的数据
Memory Profile(Heap Profile):报告程序的内存使用情况
Block Profiling:报告 goroutines 不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
Goroutine Profiling:报告 goroutines 的使用情况,有哪些 goroutine,它们的调用关系是怎样的
采集性能数据
Go语言内置了获取程序的运行数据的工具,包括以下两个标准库:
runtime/pprof
:采集工具型应用运行数据进行分析(不需要时刻运行)
net/http/pprof
:采集服务型应用运行时数据进行分析(web服务器之类需要时刻运行)
pprof开启后,每隔一段时间(通常为10ms)就会收集下当前的堆栈信息,获取各个函数占用的CPU以及内存资源;最后通过对这些采样数据进行分析,形成一个性能分析报告。
注意,我们只应该在性能测试的时候才在代码中引入pprof。
对于工具型应用
引用库import "runtime/pprof"
CPU性能分析
开启CPU性能分析:
1
pprof . StartCPUProfile ( w io . Writer )
停止CPU性能分析:
应用执行结束后,就会生成一个文件,保存了我们的 CPU profiling 数据。得到采样数据之后,使用go tool pprof
工具进行CPU性能分析。
内存性能优化
记录程序的堆栈信息
1
pprof . WriteHeapProfile ( w io . Writer )
得到采样数据之后,使用go tool pprof
工具进行内存性能分析。
go tool pprof
默认是使用-inuse_space
进行统计,还可以使用-inuse-objects
查看分配对象的数量。
对于服务型应用
如果你的应用程序是一直运行的,比如 web 应用,那么可以使用net/http/pprof
库,它能够在提供 HTTP 服务进行分析。
如果使用了默认的http.DefaultServeMux
(通常是代码直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),只需要在你的web server端代码中按如下方式导入net/http/pprof
1
import _ "net/http/pprof"
如果你使用自定义的 Mux,则需要手动注册一些路由规则:
1
2
3
4
5
r . HandleFunc ( "/debug/pprof/" , pprof . Index )
r . HandleFunc ( "/debug/pprof/cmdline" , pprof . Cmdline )
r . HandleFunc ( "/debug/pprof/profile" , pprof . Profile )
r . HandleFunc ( "/debug/pprof/symbol" , pprof . Symbol )
r . HandleFunc ( "/debug/pprof/trace" , pprof . Trace )
如果你使用的是gin框架,那么推荐使用github.com/gin-contrib/pprof ,在代码中通过以下命令注册pprof相关路由。
不管哪种方式,你的 HTTP 服务都会多出/debug/pprof
endpoint,访问它会得到类似下面的内容: 这个路径下还有几个子页面:
/debug/pprof/profile:访问这个链接会自动进行 CPU profiling,持续 30s,并生成一个文件供下载
/debug/pprof/heap: Memory Profiling 的路径,访问这个链接会得到一个内存 Profiling 结果的文件
/debug/pprof/block:block Profiling 的路径
/debug/pprof/goroutines:运行的 goroutines 列表,以及调用关系
Flag包——命令行工具(go bulid 后面的参数)
os.Args
os.Args
可以以[]string
的形式获得命令行输入的东西,但如果需要对其进行更复杂的编辑则需要用到flag
包
定义命令行flag参数
有以下两种常用的定义命令行flag
参数的方法。
flag.Type()
基本格式如下:
flag.Type(flag名, 默认值, 帮助信息)*Type
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
1
2
3
4
name := flag . String ( "name" , "张三" , "姓名" )
age := flag . Int ( "age" , 18 , "年龄" )
married := flag . Bool ( "married" , false , "婚否" )
delay := flag . Duration ( "d" , 0 , "时间间隔" )
需要注意的是,此时name
、age
、married
、delay
均为对应类型的指针。
flag.TypeVar()
基本格式如下: flag.TypeVar(Type指针, flag名, 默认值, 帮助信息)
例如我们要定义姓名、年龄、婚否三个命令行参数,我们可以按如下方式定义:
1
2
3
4
5
6
7
8
var name string
var age int
var married bool
var delay time . Duration
flag . StringVar ( & name , "name" , "张三" , "姓名" )
flag . IntVar ( & age , "age" , 18 , "年龄" )
flag . BoolVar ( & married , "married" , false , "婚否" )
flag . DurationVar ( & delay , "d" , 0 , "时间间隔" )
其中的Type()
代指任何flag允许的数据类型:
flag参数
有效值
string
合法字符串
int/int64/uint/uint64
1234、0664、0x1234等类型,也可以是负数。
float/float64
合法浮点数
bool
1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。
duration
任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。
flag.Parse()
通过以上两种方法定义好命令行flag参数后,需要通过调用flag.Parse()
来对命令行参数进行解析。
支持的命令行参数格式有以下几种:
-flag xxx
(使用空格,一个-
符号)
--flag xxx
(使用空格,两个-
符号)
-flag=xxx
(使用等号,一个-
符号)
--flag=xxx
(使用等号,两个-
符号)
其中,布尔类型的参数必须使用等号的方式指定。
Flag解析在第一个非flag参数(单个”-“不是flag参数)之前停止,或者在终止符”–“之后停止。
flag其他函数
1
2
3
flag . Args () ////返回命令行参数后的其他参数,以[]string类型
flag . NArg () //返回命令行参数后的其他参数个数
flag . NFlag () //返回使用的命令行参数个数
mySQL
建库建表
我们先在MySQL中创建一个名为db_library
的数据库 作为使用案例
1
CREATE DATABASE db_library ;
进入该数据库:
执行以下命令创建一张用于测试的数据表:
1
2
3
4
5
6
CREATE TABLE ` user ` (
` id ` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT ,
` name ` VARCHAR ( 20 ) DEFAULT '' ,
` age ` INT ( 11 ) DEFAULT '0' ,
PRIMARY KEY ( ` id ` )
) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 ;
包
1
2
3
4
5
6
7
import (
"database/sql"
"fmt"
// 隐式使用 mysql包 如果不是mysql就是其他包
_ "github.com/go-sql-driver/mysql"
)
使用MySQL驱动
1
func Open ( driverName , dataSourceName string ) ( * DB , error )
Open打开一个dirverName指定的数据库,dataSourceName指定数据源,一般至少包括数据库文件名和其它连接必要的信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var db * sql . DB
// 连接
func initDB () ( err error ) {
// DSN:Data Source Name
dsn := "root:123456@tcp(127.0.0.1:3306)/db_library"
db , err = sql . Open ( "mysql" , dsn )
if err != nil { //不会校验用户名密码等,只会检测是否提供了应该提供的信息
panic ( err )
}
err = db . Ping () //真正检验是否连接成功
if err != nil {
panic ( err )
}
fmt . Println ( "连接成功" ) // 注意这行代码要写在上面err判断的下面
db . SetMaxOpenConns ( 10 ) //设置连接池中的最大连接数,如果连接池中的连接数到达上限,就会等待连接归还后再给出连接
db . SetMaxIdleConns ( 5 ) //设置连接的最大空闲连接数,当空闲连接过多时关掉一些连接,如果最大空闲连接数大于最大连接数则会自己更改为最大连接数
return
}
CRUD(增删改查)
查询
建立结构体来存放所得数据
1
2
3
4
5
type user struct {
id int
age int
name string
}
单行查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//查询单条数据
func queryRowDemo (){
sqlStr := "select id,name,age from user where id=1;"
Obj := db . QueryRow ( sqlStr )
var u user
Obj . Scan ( & u . id , & u . name , & u . age ) //Scan函数中自带了defer Obj.Close(),会释放连接的Obj,写其他代码要注意释放,否则会导致其他并发无法访问
}
---------------------------------------------------------------------------------------------------------
func queryRowDemo ( n int ) ( u user ){
sqlStr := "select id, name, age from user where id=?" //sqlStr中的具体数据也可以使用'?'来占位
err := db . QueryRow ( sqlStr , n ). Scan ( & u . id , & u . name , & u . age ) //使用问号占位的话QueryRow()中需要再传一个查询的值
if err != nil {
fmt . Printf ( "scan failed, err:%v\n" , err )
return
}
return
}
多行查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//查询id>n的数据
func query ( n int ) {
sqlStr := "select id, name, age from user where id>?"
rows , err := db . Query ( sqlStr , n )
if err != nil {
fmt . Println ( err )
return
}
defer rows . Close ()
for rows . Next (){
var u user
err = rows . Scan ( & u . id , & u . name , & u . age )
fmt . Println ( "u:" , u )
}
}
增改查
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
//插入
func insert ( u user ){
sqlStr := `insert into user values (?,?,?)`
_ , err := db . Exec ( sqlStr , u . id , u . name , u . age )
if err != nil {
fmt . Println ( err )
return
}
fmt . Println ( "插入成功" )
}
//删除
func deleteById ( id int ){
sqlStr := `delete from user where id=?`
ret , err := db . Exec ( sqlStr , id )
if err != nil {
fmt . Println ( err )
return
}
n , _ := ret . RowsAffected ();
fmt . Println ( n , "删除成功" )
}
//修改
func updateById ( id , age int , name string ){
sqlStr := `update user set name=?,age=? where id=?`
ret , err := db . Exec ( sqlStr , name , age , id )
if err != nil {
fmt . Println ( err )
return
}
n , _ := ret . RowsAffected ();
fmt . Println ( n , "修改成功" )
}
预处理
预处理可以提前让服务器编译,一次编译多次执行,节省后续编译的成本
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 prepareQueryDemo () {
sqlStr := "select id, name,lv from user where id > ?"
stmt , err := db . Prepare ( sqlStr )
if err != nil {
fmt . Printf ( "prepare failed, err:%v\n" , err )
return
}
defer stmt . Close ()
rows , err := stmt . Query ( 0 )
if err != nil {
fmt . Printf ( "query failed, err:%v\n" , err )
return
}
defer rows . Close ()
// 循环读取结果集中的数据
for rows . Next () {
var u user
err := rows . Scan ( & u . id , & u . name , & u . id )
if err != nil {
fmt . Printf ( "scan failed, err:%v\n" , err )
return
}
fmt . Printf ( "id:%d name:%s age:%d\n" , u . id , u . name , u . id )
}
}
事务
使一堆需要同时完成的数据库操作,同进同退,如:支付宝付钱时 买家的钱减少和卖家的钱增加是比需要同时进行
思想就是用db.Begin()
记录事务开始的位置
遇到错就Rollback()
回滚并返回错误
到最后都没错就用Commit()
提交
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
// 事务操作:交换两个用户的lv
// 开启事务 func (db *DB) Begin() (*Tx, error)
// 回滚 func (tx *Tx) Rollback() error
// 提交 func (db *DB) Begin() (*Tx, error)
func exchangeUserLv ( id1 , id2 int )( err error ){
tx , err := db . Begin ()
if err != nil {
if tx != nil {
tx . Rollback ()
}
return
}
u1 , err := queryRowDemo ( id1 )
if err != nil {
return
}
u2 , err := queryRowDemo ( id2 )
if err != nil {
return
}
_ , err = updateById ( id1 , u2 . lv , u1 . name )
if err != nil {
tx . Rollback ()
return
}
_ , err = updateById ( id2 , u1 . lv , u2 . name )
if err != nil {
tx . Rollback ()
return
}
tx . Commit ()
return
}
redis
Redis是一个开源的内存数据库,Redis提供了多种不同类型的数据结构,很多业务场景下的问题都可以很自然地映射到这些数据结构上。除此之外,通过复制、持久化和客户端分片等特性,我们可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。
Redis支持的数据结构
Redis支持诸如字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、带范围查询的排序集合(sorted sets)、位图(bitmaps)、hyperloglogs、带半径查询和流的地理空间索引等数据结构(geospatial indexes)。
Redis应用场景
缓存系统,减轻主数据库(MySQL)的压力。
计数场景,比如微博、抖音中的关注数和粉丝数。
热门排行榜,需要排序的场景特别适合使用ZSET。
利用LIST可以实现队列的功能。
redis在终端中使用
cmd中打开
直接在终端中使用的话先redis-server
打开服务器,然后在开一个终端输入redis-cli
启动客户端
命令-菜鸟教程
SET 变量名 值 EX 过期时间(单位秒,可选) //写入
get 变量名 //读取
go-redis库
区别于另一个比较常用的Go语言redis client库:redigo ,我们这里采用https://github.com/go-redis/redis连接Redis数据库并进行操作,因为go-redis
支持连接哨兵及集群模式的Redis。
1
go get -u github.com/go-redis/redis
连接
==v8之前的老版本==
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 声明一个全局的rdb变量
var rdb * redis . Client
// 初始化连接
func initClient () ( err error ) {
rdb = redis . NewClient ( & redis . Options {
Addr : "localhost:6379" ,
Password : "" , // no password set
DB : 0 , // use default DB
})
_ , err = rdb . Ping (). Result ()
if err != nil {
return err
}
return nil
}
==V8新版本==
最新版本的go-redis
库的相关命令都需要传递context.Context
参数
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
package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8" // 注意导入的是新版本
)
var (
rdb * redis . Client
)
// 初始化连接
func initClient () ( err error ) {
rdb = redis . NewClient ( & redis . Options {
Addr : "localhost:6379" ,
Password : "" , // no password set
DB : 0 , // use default DB
PoolSize : 100 , // 连接池大小
})
ctx , cancel := context . WithTimeout ( context . Background (), 5 * time . Second )
defer cancel ()
_ , err = rdb . Ping ( ctx ). Result ()
return err
}
连接Redis哨兵模式
主从切换技术的方法是︰当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel (哨兵)架构来解决这个问题。
哨兵(sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。
CSDN原文链接
1
2
3
4
5
6
7
8
9
10
11
12
func initClient ()( err error ){
rdb := redis . NewFailoverClient ( & redis . FailoverOptions {
MasterName : "master" ,
SentinelAddrs : [] string { "x.x.x.x:26379" , "xx.xx.xx.xx:26379" , "xxx.xxx.xxx.xxx:26379" },
})
//v8版本以上Ping()传值同上面连接的例子
_ , err = rdb . Ping (). Result ()
if err != nil {
return err
}
return nil
}
连接Redis集群
Redis 集群是 Redis 提供的分布式数据库 方案,集群通过分片( sharding )来实现数据共享,并提供复制和故障转移。
原文链接
1
2
3
4
5
6
7
8
9
10
11
func initClient ()( err error ){
rdb := redis . NewClusterClient ( & redis . ClusterOptions {
Addrs : [] string { ":7000" , ":7001" , ":7002" , ":7003" , ":7004" , ":7005" },
})
//v8版本以上Ping()传值同上面连接的例子
_ , err = rdb . Ping (). Result ()
if err != nil {
return err
}
return nil
}
基本使用
==set/get示例==
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func redisSetGetExample () {
err := rdb . Set ( "score" , 100 , 0 ). Err () //Set(变量名,值,保存时间:0代表不超时失效)
if err != nil {
fmt . Printf ( "set score failed, err:%v\n" , err )
return
}
val , err := rdb . Get ( "score" ). Result () //返回变量名对应的值
if err != nil {
fmt . Printf ( "get score failed, err:%v\n" , err )
return
}
fmt . Println ( "score" , val )
val2 , err := rdb . Get ( "name" ). Result () //此处的"name"并没有进行过Set()操作
if err == redis . Nil {
fmt . Println ( "name does not exist" )
} else if err != nil {
fmt . Printf ( "get name failed, err:%v\n" , err )
return
} else {
fmt . Println ( "name" , val2 )
}
}
==zset示例==
zset 是 Redis 提供的最具特色的数据类型之一,首先它是一个 set,这保证了内部 value 值的唯一性,其次它给每个 value 添加了一个 score(分值)属性,通过对分值的排序实现了有序化。比如用 zset 结构来存储学生的成绩,value 值代表学生的 ID,score 则是的考试成绩。我们可以对成绩按分数进行排序从而得到学生的的名次。
原文链接
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
func redisZSetExample () {
zsetKey := "language_rank"
languages := [] redis . Z {
redis . Z { Score : 90.0 , Member : "Golang" },
redis . Z { Score : 98.0 , Member : "Java" },
redis . Z { Score : 95.0 , Member : "Python" },
redis . Z { Score : 97.0 , Member : "JavaScript" },
redis . Z { Score : 99.0 , Member : "C/C++" },
}
// ZADD
num , err := rdb . ZAdd ( zsetKey , languages ... ). Result ()
if err != nil {
fmt . Printf ( "zadd failed, err:%v\n" , err )
return
}
fmt . Printf ( "zadd %d succ.\n" , num )
// 把Golang的分数加10
newScore , err := rdb . ZIncrBy ( zsetKey , 10.0 , "Golang" ). Result ()
if err != nil {
fmt . Printf ( "zincrby failed, err:%v\n" , err )
return
}
fmt . Printf ( "Golang's score is %f now.\n" , newScore )
// 取分数最高的3个
ret , err := rdb . ZRevRangeWithScores ( zsetKey , 0 , 2 ). Result ()
if err != nil {
fmt . Printf ( "zrevrange failed, err:%v\n" , err )
return
}
for _ , z := range ret {
fmt . Println ( z . Member , z . Score )
}
// 取95~100分的
op := redis . ZRangeBy {
Min : "95" ,
Max : "100" ,
}
ret , err = rdb . ZRangeByScoreWithScores ( zsetKey , op ). Result ()
if err != nil {
fmt . Printf ( "zrangebyscore failed, err:%v\n" , err )
return
}
for _ , z := range ret {
fmt . Println ( z . Member , z . Score )
}
}
==根据前缀获取Key==
1
vals , err := rdb . Keys ( ctx , "prefix*" ). Result ()
==执行自定义命令==
1
res , err := rdb . Do ( ctx , "set" , "key" , "value" ). Result ()
NSQ
nsq官网
没弄明白,李文周老师的文章先贴一会
https://liwenzhou.com/posts/Go/go_nsq/
go module
go module
是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module
将是Go语言默认的依赖管理工具。
GO111MODULE
要启用go module
支持首先要设置环境变量GO111MODULE
,通过它可以开启或关闭模块支持,它有三个可选值:off
、on
、auto
,默认值是auto
。
GO111MODULE=off
禁用模块支持,编译时会从GOPATH
和vendor
文件夹中查找包。
GO111MODULE=on
启用模块支持,编译时会忽略GOPATH
和vendor
文件夹,只根据 go.mod
下载依赖。
GO111MODULE=auto
,当项目在$GOPATH/src
外且项目根目录有go.mod
文件时,开启模块支持。
简单来说,设置GO111MODULE=on
之后就可以使用go module
了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。
使用 go module 管理依赖后会在项目根目录下生成两个文件go.mod
和go.sum
。
GOPROXY
Go1.11之后设置GOPROXY命令为:
1
export GOPROXY = https://goproxy.cn
Go1.13之后GOPROXY
默认值为https://proxy.golang.org
,在国内是无法访问的,所以十分建议大家设置GOPROXY,这里我推荐使用goproxy.cn 。
1
go env -w GOPROXY = https://goproxy.cn,direct
go mod命令
常用的go mod
命令如下:
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖
go.mod
go.mod文件记录了项目所有的依赖信息,其结构大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
module github.com/Q1mi/studygo/blogger
go 1.12
require (
github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
github.com/gin-gonic/gin v1.4.0
github.com/go-sql-driver/mysql v1.4.1
github.com/jmoiron/sqlx v1.2.0
github.com/satori/go.uuid v1.2.0
google.golang.org/appengine v1.6.1 // indirect
)
其中,
module
用来定义包名
require
用来定义依赖包及版本
indirect
表示间接引用
依赖的版本
go mod支持语义化版本号,比如go get foo@v1.2.3
,也可以跟git的分支或tag,比如go get foo@master
,当然也可以跟git提交哈希,比如go get foo@e3702bed2
。关于依赖的版本支持以下几种格式:
1
2
3
4
5
gopkg . in / tomb . v1 v1 .0.0 - 20141024135613 - dd632973f1e7
gopkg . in / vmihailenco / msgpack . v2 v2 .9.1
gopkg . in / yaml . v2 <= v2 .2.1
github . com / tatsushid / go - fastping v0 .0.0 - 20160109021039 - d7bb493dee3e
latest
replace
在国内访问golang.org/x的各个包都需要翻墙,你可以在go.mod中使用replace替换成github上对应的库。
1
2
3
4
5
replace (
golang . org / x / crypto v0 .0.0 - 20180820150726 - 614 d502a4dac => github . com / golang / crypto v0 .0.0 - 20180820150726 - 614 d502a4dac
golang . org / x / net v0 .0.0 - 20180821023952 - 922 f4815f713 => github . com / golang / net v0 .0.0 - 20180826012351 - 8 a410e7b638d
golang . org / x / text v0 .3.0 => github . com / golang / text v0 .3.0
)
go get
在项目中执行go get
命令可以下载依赖包,并且还可以指定下载的版本。
运行go get -u
将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
运行go get -u=patch
将会升级到最新的修订版本
运行go get package@version
将会升级到指定的版本号version
如果下载所有依赖可以使用go mod download
命令。
整理依赖
我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod
文件中自动移除。这种情况下我们可以使用go mod tidy
命令更新go.mod
中的依赖关系。
go mod edit
格式化
因为我们可以手动修改go.mod文件,所以有些时候需要格式化该文件。Go提供了一下命令:
添加依赖项
1
go mod edit -require= golang.org/x/text
移除依赖项
如果只是想修改go.mod
文件中的内容,那么可以运行go mod edit -droprequire=package path
,比如要在go.mod
中移除golang.org/x/text
包,可以使用如下命令:
1
go mod edit -droprequire= golang.org/x/text
关于go mod edit
的更多用法可以通过go help mod edit
查看。
在项目中使用go module
既有项目
如果需要对一个已经存在的项目启用go module
,可以按照以下步骤操作:
在项目目录下执行go mod init
,生成一个go.mod
文件。
执行go get
,查找并记录当前项目的依赖,同时生成一个go.sum
记录每个依赖库的版本和哈希值。
新项目
对于一个新创建的项目,我们可以在项目文件夹下按照以下步骤操作:
执行go mod init 项目名
命令,在当前项目文件夹下创建一个go.mod
文件。
手动编辑go.mod
中的require依赖项或执行go get
自动发现、维护依赖。
go泛型
go 1.18+
使用[]
在func\map\struct
等定义时加入在传入时定义的类型,该功能本可以通过反射-->类型断言
来实现,但泛型无疑使它更简单了
==泛型函数==
在函数前面给出类型,[T any]
这个any是interface{}
的别名,可以在这里对传入的T类型进行限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// T 其他例子
// [T interface{int|byte|string}],T只能是int\byte\string类型
func PrintSlice [ T any ]( arr [] T ){
for _ , v := range arr {
fmt . Print ( v )
}
fmt . Println ()
}
func main () {
arr1 := [] int { 1 , 2 , 3 , 4 }
arr2 := [] byte { 'a' , 'b' , 'c' }
arr3 := [] string { "123" , "abc" }
PrintSlice [ int ]( arr1 )
PrintSlice [ byte ]( arr2 )
PrintSlice [ string ]( arr3 )
}
==泛型切片==
1
2
type arr [ T any ][] T
v := arr [ int ]{ 58 , 1881 }
==泛型map==
1
2
type M [ K string , V any ] map [ K ] V
m := M [ string , int ]{ "key" : 1 }
==泛型通道==
1
2
3
4
5
6
7
8
9
10
11
12
type C [ T any ] chan T
func main () {
c1 := make ( C [ int ], 10 )
c1 <- 1
c1 <- 2
c2 := make ( C [ string ], 10 )
c2 <- "hello"
c2 <- "world"
fmt . Println ( <- c1 , <- c2 )
}
go: … 编译指示
指令
简介
go:linkname
连接函数名和函数体,类似c++接口匹配实现需要导入 unsafe包
//go:linkname B A,函数名B的实现 对应函数A(A未必是自己包里的甚至未必是go的)
go:noescape
指定下一个有声明但没有主体(意味着实现有可能不是 Go)的函数,不允许编译器对该函数做逃逸分析
//go:noescape
go:nosplit
对该函数跳过堆栈溢出的检查, go有起始栈大小限制_StackMin=2048 即2k, 当它不够用的时候就会动态增长, 这个编译指示则是跳过该机制
//go:nosplit
go:nowritebarrierrec
告诉编译器当前函数及其调用的函数(允许递归)直到发现//go:yeswritebarrierrec
为止,若期间遇到写屏障则触发一个错误。
//go:nowritebarrierrec
go:yeswritebarrierrec
表示//go:nowritebarrierrec
的结束
//go:yeswritebarrierrec
go:noinline
该指令表示该函数禁止进行内联
//go:noinline
go:norace
禁止进行竞态检测,而另外一种常见的形式就是在启动时执行 go run -race
,能够检测应用程序中是否存在双向的数据竞争
//go:norace
go:notinheap
该指令常用于类型声明,它表示这个类型不允许从 GC堆上进行申请内存 在运行时中常用其来做较低层次的内部结构,避免调度器和内存分配中的写屏障 能够提高性能
//go:notinheap
go:systemstack
表示函数必须在系统栈上运行
//go:systemstack
go日志
Logger
zap
Zap提供了两种类型的日志记录器—Sugared Logger
和Logger
。
在性能很好但不是很关键的上下文中,使用SugaredLogger
。它比其他结构化日志记录包快4-10倍,并且支持结构化和printf风格的日志记录。
在每一微秒和每一次内存分配都很重要的上下文中,使用Logger
。它甚至比SugaredLogger
更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
Logger
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
package main
import (
"go.uber.org/zap"
"net/http"
)
var logger * zap . Logger
func initLogger () {
logger , _ = zap . NewProduction ()
//logger, _ = zap.NewDevelopment()
//logger = zap.NewExample()
}
func simpleHttpGet ( url string ) {
resp , err := http . Get ( url )
if err != nil {
// Error处允许 Info/Error/Debug/Panic
// 第1个参数 消息字符串 此处的"Error fetching url.."极为对错误信息的简单描述
// 第2个参数为 zapcore.Field切片, zapcore.Field是对一些信息的包装类,大多是惰性传递,不处理的说明信息,zap.String()\zap.Error()都是将一些信息转成 Field的函数
logger . Error (
"Error fetching url.." ,
zap . String ( "url" , url ),
zap . Error ( err ))
} else {
logger . Info ( "Success.." ,
zap . String ( "statusCode" , resp . Status ),
zap . String ( "url" , url ))
resp . Body . Close ()
}
}
func main () {
initLogger ()
defer logger . Sync ()
simpleHttpGet ( "www.google.com" )
simpleHttpGet ( "http://www.google.com" )
}
运行结果:
{"level":"error","ts":1655169706.2337923,"caller":"ginStudy/main.go:20","msg":"Error fetching url..","url":"www.google.com","error":"Get \"www.google.com\": unsupported protocol scheme \"\"","stacktrace":"main.simpleHttpGet\n\tE:/实验/go源码/ginStudy/main.go:20\nmain.main\n\tE:/实验/go源码/ginStudy/main.go:35\nruntime.main\n\tD:/go/go1.17.10/src/runtime/proc.go:255"}
{"level":"info","ts":1655169706.9466245,"caller":"ginStudy/main.go:25","msg":"Success..","statusCode":"200 OK","url":"http://www.google.com"}
返回值解释
level 返回值类型 由 logger.Info\logger.Error等决定,除了这两个还可以是 Debug和Panic
ts 即 timestamp 时间戳
caller 该信息写入的地方
msg 写入的提示信息
Sugared Logger
约束类型
1
2
3
4
5
6
7
// any:1.18新特性,interface{}的别名
type any = interface {}
// comparable:1.18新特性,表示支持==和!=的类型,数字\string\自定义结构体
type comparable interface { comparable }
type rune = int32
type byte = uint8
交叉编译(windows)
1
2
3
4
SET CGO_ENABLED = 0 # 用于标识(声明) cgo 工具是否可用
SET GOOS = darwin # 用于标识(声明)程序构建环境的目标操作系统
SET GOARCH = amd64 # 声明程序构建环境的目标计算架构 若不设置,默认值与程序运行环境的目标计算架构一致
go build main.go
1
2
3
4
SET CGO_ENABLED = 0
SET GOOS = linux
SET GOARCH = amd64
go build main.go
GOOS:目标平台的操作系统(darwin、freebsd、linux、windows) GOARCH:目标平台的体系架构(386、amd64、arm) 交叉编译不支持 CGO 所以要禁用它
程序恢复(recover)
Go:程序如何恢复(recover)? - 知乎 (zhihu.com)
go注解
go:embed
embed是在Go 1.16中新加包。它通过//go:embed
指令,可以在编译阶段将静态资源文件打包进编译好的程序中,并提供访问这些文件的能力
==基本使用==
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
_ "embed"
"fmt"
)
// 不能写在函数里 必须定义在外面
// 前面不能有空格 有了空格就会把它当成注释
//go:embed a.txt
var a string
func main (){
fmt . Println ( a )
}
==三种数据类型==
在embed中,可以将静态资源文件嵌入到三种类型的变量,分别为:字符串、字节数组、embed.FS文件类型
字符串 就像上面的例子
字节数组 就是用[]byte 类型代替 string类型 实际上是一样的
embed.FS 文件类型
1
2
3
4
// 它在源码里就是一个文件切片指针
type FS struct {
files * [] file
}
1
2
3
4
5
6
7
8
9
10
// 它包含的三种可调用方法
// Open 打开要读取的文件,并返回文件的fs.File结构.
func ( f FS ) Open ( name string ) ( fs . File , error )
// ReadDir 读取并返回整个命名目录
func ( f FS ) ReadDir ( name string ) ([] fs . DirEntry , error )
// ReadFile 读取并返回name文件的内容.
func ( f FS ) ReadFile ( name string ) ([] byte , error )
go:linkname
在Go中,go:linkname指令用于将Go函数与其他包中的函数进行链接
语法://go:linkname localname importpath.name
localname 当前包中的函数名
importpath.name 要链接的外部包中的函数名 使用这个指令可以在当前包中访问和调用其他包中的私有函数,或者将一个公共函数替换为另一个实现
例如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import (
"fmt"
// go:linkname 依赖于unsafe包
_ "unsafe"
)
func main () {
fmt . Println ( add1 ( 1 , 2 ))
}
// add1 使用了go:linkname指令来链接到runtime包中的add函数
//go:linkname add1 runtime.add
func add1 ( x , y int ) int
除了go:linkname之外,Go还提供了其他几个与链接相关的指令,它们的语法格式和作用如下:
注意:下面的几个相关指令为chatGPT的答案 我并没有试过(不擅长c 也不擅长汇编 抱歉啦 >_< )
go:linkshared
这个指令用于将包中的函数和变量链接到共享库中,以便在运行时动态加载和链接。它可以用于构建插件、扩展和动态库等。
go:cgo_export_static和go:cgo_export_dynamic
1
2
//go:cgo_export_static localname
//go:cgo_export_dynamic localname
这两个指令用于将C语言中的函数和变量导出到Go包中,以便在Go代码中调用。它们主要用于实现Go和C语言之间的互操作。
go:cgo_import_dynamic
1
//go:cgo_import_dynamic symbol lib
这个指令用于在运行时动态加载和链接C语言中的函数和变量。它可以用于实现C语言的扩展、插件和动态库等。
其他工具:
json
encoding/json标准库是使用:在Golang(GO)中使用JSON——实例解析指南 - 掘金 (juejin.cn)