当前位置: 首页 > 工具软件 > golang-101 > 使用案例 >

golang-语法基础

阎博易
2023-12-01

Go Module

  • go env -w GOPROXY="https://goproxy.cn,direct"

get

  • -u:下载并安装代码包,不论工作区中是否已存在它们
  • -t:同时下载测试所需的代码包

编译

  • 构建使用命令go build,安装使用命令go install 构建和安装代码包的时候都会执行编译、打包等操作,并且,这些操作生成的任何文件都会先被保存到某个临时的目录中
    • 如果构建的是库源码文件,那么操作后产生的结果文件只会存在于临时目录中。这里的构建的主要意义在于检查和验证
      • 库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用
    • 如果构建的是命令源码文件(main),那么操作的结果文件会被搬运到源码文件所在的目录中
      • 如果一个源码文件声明属于main包,并且包含一个无参数声明且无结果声明的main函数,那么它就是命令源码文件(对于一个独立的程序来说,命令源码文件永远只会也只能有一个。如果有与命令源码文件同包的源码文件,那么它们也应该声明属于main包)
    • 如果安装的是库源码文件,那么结果文件会被搬运到它所在工作区的 pkg 目录下的某个子目录中
    • 如果安装的是命令源码文件,那么结果文件会被搬运到它所在工作区的 bin 目录中
  • .a文件是编译过程中生成的,每个package都会生成对应的.a文件,Go在编译的时候先判断package的源码是否有改动,如果没有的话,就不再重新编译.a文件,这样可以加快速度
  • 在运行go build命令的时候,默认不会编译目标代码包所依赖的那些代码包。当然,如果被依赖的代码包的归档文件不存在,或者源码文件有了变化,那它还是会被编译
    • 如果要强制编译它们,可以在执行命令的时候加入标记-a。此时,不但目标代码包总是会被编译,它依赖的代码包也总会被编译,即使依赖的是标准库中的代码包也是如此
    • 另外,如果不但要编译依赖的代码包,还要安装它们的归档文件,那么可以加入标记-i
    • 那么我们怎么确定哪些代码包被编译了呢?有两种方法
      • 运行go build命令时加入标记-x,这样可以看到go build命令具体都执行了哪些操作。也可以加入标记-n,这样可以只查看具体操作而不执行它们
      • 运行go build命令时加入标记-v,这样可以看到go build命令编译的代码包的名称。它在与-a标记搭配使用时很有用

初始化

  • init 的顺序由实际包调用顺序给出,所有引入的外部包的 init 均会被编译器安插在当前包的 main.init 之前执行
  • 一个包内的 init 函数的调用顺序取决于声明的顺序,即从上而下依次调用
  • 不管包被导入多少次,包内的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) 

}

for 和range

循环永动机

// 请问如下程序是否能正常结束?
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

  • range遍历字符串**时会把对应的字节并将字节转换成 **rune
  • range遍历slice,由于循环开始前循环次数就已经确定了,所以循环过程中新添加的元素是没办法遍历到的
  • range遍历slice,由于map底层实现与slice不同,map底层使用hash表实现,插入数据位置是随机的,所以遍历过程中新插入的数据不能保证遍历到
    • hash表的遍历就是遍历所有的桶(起始桶的位置是随机的),所以新添加的元素可能出现在之前遍历过的桶中,这样就会有些新添加的元素可能遍历不到
    • https://segmentfault.com/q/1010000012242735
  • range遍历 channel是依次从channel中读取数据,读取前是不知道里面有多少个元素的。如果channel中没有元素,则会阻塞等待,如果channel已被关闭,则会解除阻塞并退出循环。
    • 作用于nil channel 会永久阻塞

函数

函数声明

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))
}

闭包

  • 无论是for循环,还是range迭代,其定义的局部变量都会被重复使用(python 不会这样),这对闭包存在一定的影响
data := [3]string{"a", "b", "c"}
for i, s := range data {
  println(&i, &s)
}
/*
0xc000077ee8 0xc000077f00
0xc000077ee8 0xc000077f00
0xc000077ee8 0xc000077f00
*/
func 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
}

结构体

  • 匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
  • go 并没有实现继承,组合优于继承
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,而且其的值为 &{}

指针

  • go 语言中对指针的限制
    • 指针不能参与运算
    • 不同类型的指针不允许相互转换
    • 不同类型的指针不能比较和相互赋值
  • Go 语言在 unsafe 包里通过 unsafe.Pointer 提供了通用指针,通过这个通用指针以及 unsafe 包的其他几个功能可以让使用者绕过 Go 语言的类型系统直接操作内存,例如:指针类型转换,读写结构体私有成员这样的操作
  • unsafe包只有两个类型,三个函数,但是功能很强大
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

ArbitraryTypeint的一个别名,在 Go 中ArbitraryType有特殊的意义。代表一个任意Go表达式类型。Pointerint指针类型的一个别名,在 Go 中可以把任意指针类型转换成unsafe.Pointer类型

三个函数的参数均是ArbitraryType类型,就是接受任何类型的变量

  • Sizeof接受任意类型的值(表达式),返回其占用的字节数
  • Offsetof:返回结构体成员在内存中的位置距离结构体起始处的字节数,所传参数必须是结构体的成员(结构体指针指向的地址就是结构体起始处的地址,即第一个成员的内存地址)
  • Alignof返回变量对齐字节数量,这个函数虽然接收的是任何类型的变量,但是有一个前提,就是变量要是一个struct类型,且还不能直接将这个struct类型的变量当作参数,只能将这个struct类型变量的值当作参数

以上三个函数返回的结果都是 uintptr 类型,这和 unsafe.Pointer 可以相互转换。三个函数都是在编译期间执行

unsafe.Pointer

unsafe.Pointer称为通用指针,官方文档对该类型有四个重要描述:

  1. 任何类型的指针都可以被转化为 unsafe.Pointer
  2. unsafe.Pointer 可以被转化为任何类型的指针;
  3. uintptr 可以被转化为 unsafe.Pointer
  4. 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

uintptr

  • uintptr是 Go 语言的内置类型,是能存储指针的整型,在64位平台上底层的数据类型是 uint64
  • 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

读写结构体的私有成员

  • 通过 Offsetof 方法可以获取结构体成员的偏移量,进而获取成员的地址,读写该地址的内存,就可以达到改变成员值的目的
  • 这里有一个内存分配相关的事实:结构体会被分配一块连续的内存,结构体的地址也代表了第一个成员的地址
  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 零拷贝转换

string和[]byte 在运行时的类型表示为reflect.StringHeaderreflect.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
}

上面程序的错误处理暴露了两个问题:

  1. 底层函数WriteAll在发生错误后,除了向上层返回错误外还向日志里记录了错误,上层调用者做了同样的事情,记录日志然后把错误再返回给程序顶层,因此在日志文件中得到一堆重复的内容
  2. 在程序的顶部,虽然得到了原始错误,但没有相关内容,换句话说没有把WriteAllWriteConfig记录到log里的那些信息包装到错误里,返回给上层

针对这两个问题的解决方案可以是,在底层函数WriteAllWriteConfig中为发生的错误添加上下文信息,然后将错误返回上层,由上层程序最后处理这些错误

一种简单的保证错误的方法是使用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)
    }
}

总结

错误处理的原则就是:

错误只在逻辑的最外层处理一次,底层只返回错误

底层除了返回错误外,要对原始错误进行包装,增加错误信息、调用栈等这些利于排查的上下文信息

异常捕获

  • recover函数的返回值是panic中传递的参数
  • 如果没有使用 recover , panic 会一直向上抛
    • 如果被调用函数自己处理了异常,不影响调用函数,只是自己终止,返回到调用函数
    • 如果被调用函数自己没有处理异常,会一直向上抛,直到遇到一个处理异常的,相当于此处出现panic
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函数就能让函数按照正常的流程执行

  • panic 只会触发当前 Goroutine 的 defer;
 类似资料: