go env -w GOPROXY="https://goproxy.cn,direct"
main.init
之前执行/*
%v:默认格式输出
%f:浮点数输出
%s:字符串输出 以string格式打印,比如打印值是[]byte的时候 []byte{0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD}
%t:布尔值输出
%c:字符输出
%p:指针输出,十六进制方式显示
%b:整型以二进制方式显示
%o:整型以八进制方式显示
%d:整数以十进制方式显示
%x:整型以十六进制方式显示
整数字面量
0xF 十六进制表示(必须使用0x或者0X开头)
0XF
017 八进制表示(必须使用0、0o或者0O开头)
0o17
0O17
0b1111 二进制表示(必须使用0b或者0B开头)
0B1111
15 十进制表示(必须不能用0开头)
*/
func main() {
//p := Person{"hang", 12}
//fmt.Printf("%v", p) // {hang 12}
//fmt.Printf("%+v",p) // {Name:hang Age:12}
//fmt.Printf("%#v", p) // main.Person{name:"hang", age:27}
//fmt.Printf("%T", p) // main.Person
//a := []byte{0xE4, 0xBD, 0xA0, 0xE5, 0xA5, 0xBD}
//fmt.Printf("%v ----- %s", a, a)
f := 3.1415926
fmt.Printf("%.2f", f)
}
循环永动机
// 请问如下程序是否能正常结束?
1. func main() {
2. v := []int{1, 2, 3}
3. for i:= range v {
4. v = append(v, i)
5. }
6. }
能够正常结束。循环内改变切片的长度,不影响循环次数,循环次数在循环开始前就已经确定了(python不行,不支持遍历期间改变列表和字典的大小,会抛出异常),仅限于for-range语法,for不可以(for会无限添加)
对于所有的 range 循环,Go 语言都会在编译期将原切片或者数组赋值给一个新变量 ha
,在赋值的过程中就发生了拷贝,而我们又通过 len
关键字预先获取了切片的长度,所以在循环中追加新的元素也不会改变循环执行的次数
神奇的指针
func main() {
arr := []int{1, 2, 3}
newArr := []*int{}
for _, v := range arr {
newArr = append(newArr, &v)
}
for _, v := range newArr {
fmt.Println(*v)
}
}
// 3 3 3
正确的做法应该是使用 &arr[i]
替代 &v
这种同时遍历索引和元素的 range 循环时,Go 语言会额外创建一个新的 v2
变量存储切片中的元素,循环中使用的这个变量 v2 会在每一次迭代被重新赋值而覆盖,也就是指针指向每次循环中的同一个变量v
rune
函数声明
type Printer func(contents string) (n int, err error)
func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}
func main() {
var p Printer
p = printToStd
p("something")
}
高阶函数
type operate func(x, y int) int
func calculate(x, y int, op operate) (int, error){
if op == nil {
return 0, errors.New("invalid operate")
}
return op(x,y), nil
}
func main() {
// 此处使用匿名函数
op := func(x,y int) int {
return x + y
}
fmt.Println(calculate(1,2, op))
}
闭包
data := [3]string{"a", "b", "c"}
for i, s := range data {
println(&i, &s)
}
/*
0xc000077ee8 0xc000077f00
0xc000077ee8 0xc000077f00
0xc000077ee8 0xc000077f00
*/
goroutine
会存在什么问题? https://www.nowcoder.com/discuss/730415?source_id=discuss_experience_nctrack&channel=-1func main() {
for i := 0; i < 3; i++ {
println(i, &i)
// 闭包传递
defer func(){ println(i, &i) } ()
}
}
func main() {
for i := 0; i < 3; i++ {
println(i, &i)
// 函数传递
defer func(i int){ println(i, &i) } (i)
}
}
func main() {
for i := 0; i < 3; i++ {
// 重新赋值
x := i
defer func(){ println(x, &x) } ()
}
}
可变参数
func sum(args ...int) int {
var result int
for _, v := range args {
result += v
}
return result
}
func Sum(args ...int) int {
// 利用 ... 来解序列
result := sum(args...)
return result
}
type Animal struct {
name string
}
func (a *Animal) run() {
fmt.Printf("%v 会跑 \n", a.name)
//fmt.Printf(a.age) // 只能访问Animal中的属性, python继承可以
fmt.Printf("%#v", a) // &main.Animal{name:"阿奇"}
}
type Dog struct {
age int
*Animal //不是匿名的也可以,不是指针类型也可以(匿名的才更像继承)
}
func (d *Dog) fei() {
fmt.Printf("%v 会汪汪叫 \n", d.name)
fmt.Printf("%v", d.age)
}
func main() {
d1 := Dog{
age: 5,
Animal: &Animal{"阿奇"},
}
d1.run()
d1.fei()
}
结构体和nil
type Student struct {}
student := new(Student)
fmt.Printf("student 的数据类型为:%T,值为:%v\n", student, student)
fmt.Println("student == nill :", student == nil)
student 的数据类型为:*main.Student,值为:&{}
student == nill : false var student *Student 才是 true
可以看到,空结构体student并不是nil,而且其的值为 &{}
type ArbitraryType int
type Pointer *ArbitraryType
func Sizeof(x ArbitraryType) uintptr
func Offsetof(x ArbitraryType) uintptr
func Alignof(x ArbitraryType) uintptr
// 内建类型,就像int
type uintptr uintptr
ArbitraryType
是int
的一个别名,在 Go 中ArbitraryType
有特殊的意义。代表一个任意Go
表达式类型。Pointer
是int
指针类型的一个别名,在 Go 中可以把任意指针类型转换成unsafe.Pointer
类型
三个函数的参数均是ArbitraryType
类型,就是接受任何类型的变量
Sizeof
接受任意类型的值(表达式),返回其占用的字节数Offsetof
:返回结构体成员在内存中的位置距离结构体起始处的字节数,所传参数必须是结构体的成员(结构体指针指向的地址就是结构体起始处的地址,即第一个成员的内存地址)Alignof
返回变量对齐字节数量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct
类型,且还不能直接将这个struct
类型的变量当作参数,只能将这个struct
类型变量的值当作参数以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行
unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:
unsafe.Pointer
;unsafe.Pointer
可以被转化为任何类型的指针;uintptr
可以被转化为 unsafe.Pointer
;unsafe.Pointer
可以被转化为 uintptr
在Go 语言中是用于各种指针相互转换的桥梁,它可以持有任意类型变量的地址,什么叫"可以持有任意类型变量的地址"呢?意思就是使用 unsafe.Pointer 转换的变量,该变量一定要是指针类型,否则编译会报错
a := 1
b := unsafe.Pointer(a) //报错
b := unsafe.Pointer(&a) // 正确
unsafe.Pointer 指针支持和 nil
比较判断是否为空指针
unsafe.Pointer 不能直接进行数学运算,但可以把它转换成 uintptr,对 uintptr 类型进行数学运算,再转换成 unsafe.Pointer 类型
// uintptr、unsafe.Pointer和普通指针之间的转换关系
uintptr <==> unsafe.Pointer <==> *T
unsafe.Pointer
指针可以被转化为uintptr
类型,然后保存到uintptr
类型的变量中(注:这个变量只是和当前指针有相同的一个数字值,并不是一个指针),然后用以做必要的指针数值运算。(uintptr是一个无符号的整型数,足以保存一个地址)这种转换虽然也是可逆的,但是随便将一个 uintptr
转为 unsafe.Pointer
指针可能会破坏类型系统,因为并不是所有的数字都是有效的内存地址i := 10
var p *int = &i
var fp *float32 = (*float32)(unsafe.Pointer(p))
*fp = *fp * 10.12
fmt.Println(i) // 101
这里,我们将指向 int
类型的指针转化为了 unsafe.Pointer
类型,再转化为 *float32
类型,并进行运算,最后发现 i
的值发生了改变。
// 可以通过 unsafe.Pointer 和 uintptr 进行转换,得到 slice 的字段值
type slice struct {
array unsafe.Pointer //元素指针
len int
cap int
}
s :make([]int,9,20)
var Len *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))+uintptr(8)))
fmt.Println(Len,len(s))// 9 9
var Cap *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))+uintptr(16)))
fmt.Println(Cap,cap(s))// 20 20
var x struct {
a int
b int
c []int
}
// unsafe.Offsetof 函数的参数必须是一个字段, 比如 x.b, 方法会返回 b 字段相对于 x 起始地址的偏移量, 包括可能的空洞。
// 指针运算 uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)。
// 和 pb := &x.b 等价
pb := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
上面的写法尽管很繁琐,但在这里并不是一件坏事,因为这些功能应该很谨慎地使用。不要试图引入一个uintptr类型的临时变量,因为它可能会破坏代码的安全性
如果改为下面这种用法是有风险的:
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
*pb = 42
随着程序执行的进行,goroutine 会经常发生栈扩容或者栈缩容,会把旧栈内存的数据拷贝到新栈区然后更改所有指针的指向。一个 unsafe.Pointer 是一个指针,因此当它指向的数据被移动到新栈区后指针也会被更新。但是uintptr 类型的临时变量只是一个普通的数字,所以其值不会该被改变。上面错误的代码因为引入一个非指针的临时变量 tmp
,导致系统无法正确识别这个是一个指向变量 x 的指针。当第二个语句执行时,变量 x 的数据可能已经被转移,这时候临时变量tmp
也就不再是现在的 &x.b
的地址。第三个语句向之前无效地址空间的赋值语句将让整个程序崩溃
string和[]byte 在运行时的类型表示为reflect.StringHeader
和reflect.SliceHeader
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
type StringHeader struct {
Data uintptr
Len int
}
只需要共享底层 []byte 数组就可以实现零拷贝转换
func main() {
s := "Hello World"
b := string2bytes(s)
fmt.Println(b)
s = bytes2string(b)
fmt.Println(s)
}
func string2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
func bytes2string(b []byte) string {
sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := reflect.StringHeader{
Data: sliceHeader.Data,
Len: sliceHeader.Len,
}
return *(*string)(unsafe.Pointer(&sh))
}
error
接口,这样就不用向调用方暴露额外的类型myError
这个类型,如果不实现error
接口的话,调用者的代码中就会被myError
这个类型侵入。即,下面的run
函数,在定义返回值类型时,直接定义成error即可package myerror
type myError struct {
Code int
When time.Time
What string
}
func (e *myError) Error() string {
return fmt.Sprintf("at %v, %s",e.When, e.What)
}
func run() error {
return &MyError{
1002,
time.Now(),
"it didn't work",
}
}
error
是具体哪种错误的时候应该怎么办呢,myError
并未向包外暴露,答案是通过向包外暴露检查错误行为的方法来实现myerror.IsXXXError(err)
error
本身与包向外暴露的常量错误是否相等来判断,比如操作文件时常用来判断文件是否结束的io.EOF
if err != io.EOF {
return err
}
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err)
return err
}
return nil
}
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
return err
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}
func main() {
err := WriteConfig(f, &conf)
fmt.Println(err) // io.EOF
}
上面程序的错误处理暴露了两个问题:
WriteAll
在发生错误后,除了向上层返回错误外还向日志里记录了错误,上层调用者做了同样的事情,记录日志然后把错误再返回给程序顶层,因此在日志文件中得到一堆重复的内容WriteAll
、WriteConfig
记录到log里的那些信息包装到错误里,返回给上层针对这两个问题的解决方案可以是,在底层函数WriteAll
、WriteConfig
中为发生的错误添加上下文信息,然后将错误返回上层,由上层程序最后处理这些错误
一种简单的保证错误的方法是使用fmt.Errorf
函数,给错误添加信息
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
return fmt.Errorf("could not marshal config: %v", err)
}
if err := WriteAll(w, buf); err != nil {
return fmt.Errorf("could not write config: %v", err)
}
return nil
}
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
return fmt.Errorf("write failed: %v", err)
}
return nil
}
fmt.Errorf
只是给错误添加了简单的注解信息,如果你想在添加信息的同时还加上错误的调用栈,可以借助github.com/pkg/errors
这个包,提供的包装错误的能力
//只附加新的信息
func WithMessage(err error, message string) error
//只附加调用堆栈信息
func WithStack(err error) error
//同时附加堆栈和信息
func Wrap(err error, message string) error
有包装方法,就有对应的解包方法,Cause方法会返回包装错误对应的最原始错误–即会递归地进行解包
func Cause(err error) error
下面是使用github.com/pkg/errors
改写后的错误处理程序
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}
func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.WithMessage(err, "could not read config")
}
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace:\n%+v\n", err)
os.Exit(1)
}
}
错误处理的原则就是:
错误只在逻辑的最外层处理一次,底层只返回错误
底层除了返回错误外,要对原始错误进行包装,增加错误信息、调用栈等这些利于排查的上下文信息
func main() {
a()
}
func a() {
defer b()
panic("a panic")
}
func b() {
defer fb()
panic("b panic")
}
func fb() {
panic("fb panic")
}
panic: a panic
panic: b panic
panic: fb panic
最终程序先打印最早出现的panic,再打印其他的panic,嵌套panic不会陷入死循环,每个defer函数都只会被调用一次
将上面的程序稍微改进一下,让main函数捕获嵌套的panic
func main() {
defer catch("main")
a()
}
func a() {
defer b()
panic("a panic")
}
func b() {
defer fb()
panic("b panic")
}
func fb() {
panic("fb panic")
}
func catch(funcname string) {
if r := recover(); r != nil {
fmt.Println(funcname, "recover:", r)
}
}
最终程序的输出结果为main recover:fb panic,这意味着recover函数最终捕获的是最近发生的panic,即便有多个panic函数,在最上层的函数也只需要一个recover函数就能让函数按照正常的流程执行