学习golang语言时遇到的难点语法

作者是java选手,实习需要转go,记录学习go中遇到的一些与java不同的语法。

defer

defer特性

  1. 关键字 defer 用于注册延迟调用。

  2. 这些调用直到 return 前才被执。因此,可以用来做资源清理

  3. 多个defer语句,按先进后出的方式执行。

  4. defer语句中的变量,在defer声明时就决定了。

defer用途:

  1. 关闭文件句柄

  2. 锁资源释放

  3. 数据库连接释放

defer 与 closure

Go 复制代码
package main

import (
    "errors"
    "fmt"
)

func foo(a, b int) (i int, err error) {
    defer fmt.Printf("first defer err %v\n", err)
    defer func(err error) { fmt.Printf("second defer err %v\n", err) }(err)
    defer func() { fmt.Printf("third defer err %v\n", err) }()
    if b == 0 {
        err = errors.New("divided by zero!")
        return
    }

    i = a / b
    return
}

func main() {
    foo(2, 0)
}  


输出结果:
    third defer err divided by zero!
    second defer err <nil>
    first defer err <nil>

结论:

在Go语言中,defer语句的参数是在defer声明时进行求值的 ,而不是在defer执行时。这意味着如果你在defer后面跟的不是闭包(closure),而是直接使用变量或表达式,那么这个值将会在defer语句被声明的那一刻就被确定下来,并且这个值在后续的函数执行过程中不会改变,即使变量本身的值发生了变化。

defer 与 return

Go 复制代码
package main

import "fmt"

func foo() (i int) {

    i = 0
    defer func() {
        fmt.Println(i)
    }()

    return 2
}

func main() {
    foo()
}

输出结果:2

结论:

retrun 2 相当于先将2赋值给i,然后返回i。而 defer 语句确保了闭包在函数返回之前执行,闭包打印的是 i 的最新值。

异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

recover捕获异常生效场景

Go 复制代码
package main

import "fmt"

func test() {
    defer func() {
        fmt.Println(recover()) //有效
    }()
    defer recover()              //无效!
    defer fmt.Println(recover()) //无效!
    defer func() {
        func() {
            println("defer inner")
            recover() //无效!
        }()
    }()

    panic("test panic")
}

func main() {
    test()
}
输出
    defer inner
    <nil>
    test panic

分析:

  • 第一个延迟调用是一个匿名函数,它内部调用了recover()。这是有效的,因为它直接在延迟调用的函数内部调用了recover()。但是,由于这是第一个执行的延迟调用,恐慌还没有被捕获,所以recover()返回nil。输出是 <nil>

  • 第二个延迟调用是直接调用recover()。这是无效的,因为recover()不是在延迟调用的函数内部调用的。因此,它不会捕获恐慌,也不会影响恐慌的处理。

  • 第三个延迟调用是fmt.Println(recover())。这也是无效的,因为recover()调用不是在延迟调用的函数内部,而是在fmt.Println的参数中。因此,它同样不会捕获恐慌。

  • 第四个延迟调用是一个匿名函数,它内部又定义了一个匿名函数并执行。内部的匿名函数打印了"defer inner",然后调用了recover()。但是,这个recover()调用是在一个嵌套的函数中,而不是直接在延迟调用的函数中,所以它也是无效的。

结论:

recover函数用于捕获恐慌(panic)。但是,recover只有在延迟调用的函数(deferred function)内部直接调用时才会捕获到恐慌 。如果recover不在延迟调用的函数内部直接调用,或者不是在延迟调用的函数中调用,它将不会捕获任何恐慌,并且总是返回nil

Go实现类似 try catch 的异常处理

Go 复制代码
package main

import "fmt"
//传入参数为可能触发panic的函数fun,处理panic的函数handler
func Try(fun func(), handler func(interface{})) {
    defer func() {
//捕获异常
        if err := recover(); err != nil {
            handler(err)
        }
    }()
    fun()
}

func main() {
    Try(func() {
        panic("test panic")
    }, func(err interface{}) {
        fmt.Println(err)
    })
} 

输出结果:test panic

如何区别使用 panic 和 error 两种方式?

惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。

单元测试

基准测试

基准测试函数格式

基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下:

Go 复制代码
​
func BenchmarkName(b *testing.B){
    // ...
} 

基准测试以Benchmark为前缀,需要一个*testing.B类型的参数b,基准测试必须要执行b.N次,这样的测试才有对照性,b.N的值是系统根据实际情况去调整的,从而保证测试的稳定性。

基准测试示例

我们为split包中的Split函数编写基准测试如下:

Go 复制代码
func BenchmarkSplit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Split("枯藤老树昏鸦", "老")
    }
}

基准测试并不会默认执行,需要增加-bench参数,所以我们通过执行go test -bench=Split命令执行基准测试,输出结果如下:

bash 复制代码
  split $ go test -bench=Split
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               203 ns/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       2.255s

其中BenchmarkSplit-8表示对Split函数进行基准测试,数字8表示GOMAXPROCS的值,这个对于并发基准测试很重要。10000000和203ns/op表示每次调用Split函数耗时203ns,这个结果是10000000次调用的平均值。

我们还可以为基准测试添加-benchmem参数,来获得内存分配的统计数据。

bash 复制代码
    split $ go test -bench=Split -benchmem
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               215 ns/op             112 B/op          3 allocs/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       2.394s

其中,112 B/op表示每次操作内存分配了112字节,3 allocs/op则表示每次操作进行了3次内存分配。 我们将我们的Split函数优化如下:

Go 复制代码
func Split(s, sep string) (result []string) {
    result = make([]string, 0, strings.Count(s, sep)+1)
    i := strings.Index(s, sep)
    for i > -1 {
        result = append(result, s[:i])
        s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度
        i = strings.Index(s, sep)
    }
    result = append(result, s)
    return
}

这一次我们提前使用make函数将result初始化为一个容量足够大的切片,而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升:

bash 复制代码
  split $ go test -bench=Split -benchmem
    goos: darwin
    goarch: amd64
    pkg: github.com/pprof/studygo/code_demo/test_demo/split
    BenchmarkSplit-8        10000000               127 ns/op              48 B/op          1 allocs/op
    PASS
    ok      github.com/pprof/studygo/code_demo/test_demo/split       1.423s

这个使用make函数提前分配内存的改动,减少了2/3的内存分配次数,并且减少了一半的内存分配。

压力测试

Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。

另外建议安装gotests插件自动生成测试代码:

bash 复制代码
  go get -u -v github.com/cweill/gotests/...   

如何编写压力测试

压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点:

压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母

Go 复制代码
 func BenchmarkXXX(b *testing.B) { ... }  

go test不会默认执行压力测试的函数,如果要执行压力测试需要带上参数-test.bench,语法:-test.bench="test_name_regex",例如go test -test.bench=".*"表示测试全部的压力测试函数

在压力测试用例中,请记得在循环体内使用testing.B.N,以使测试可以正常的运行

文件名也必须以_test.go结尾

下面我们新建一个压力测试文件webbench_test.go,代码如下所示:

Go 复制代码
package gotest

import (
    "testing"
)

func Benchmark_Division(b *testing.B) {
    for i := 0; i < b.N; i++ { //use b.N for looping 
        Division(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) {
    b.StopTimer() //调用该函数停止压力测试的时间计数

    //做一些初始化的工作,例如读取文件数据,数据库连接之类的,
    //这样这些时间不影响我们测试函数本身的性能

    b.StartTimer() //重新开始时间
    for i := 0; i < b.N; i++ {
        Division(4, 5)
    }
}  

我们执行命令go test webbench_test.go -test.bench=".*",可以看到如下结果:

bash 复制代码
    Benchmark_Division-4                            500000000          7.76 ns/op         456 B/op          14 allocs/op
    Benchmark_TimeConsumingFunction-4            500000000          7.80 ns/op         224 B/op           4 allocs/op
    PASS
    ok      gotest    9.364s   

上面的结果显示我们没有执行任何TestXXX的单元测试函数,显示的结果只执行了压力测试函数,第一条显示了Benchmark_Division执行了500000000次,每次的执行平均时间是7.76纳秒,第二条显示了Benchmark_TimeConsumingFunction执行了500000000,每次的平均执行时间是7.80纳秒。最后一条显示总共的执行时间。

方法

方法集

Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。

    • 类型 T 方法集包含全部 receiver T 方法。
    • 类型 *T 方法集包含全部 receiver T + *T 方法。
    • 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。 
    • 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。 
    • 不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。

表达式

Golang 表达式 :根据调用者不同,方法分为两种表现形式:

instance.method(args...) ---> <type>.func(instance, args...)  

前者称为 method value,后者 method expression。

两者都可像普通函数那样赋值和传参,区别在于 method value绑定实例 ,而 method expression 则须显式传参

package main

import "fmt"

type User struct {
    id   int
    name string
}

func (self *User) Test() {
    fmt.Printf("%p, %v\n", self, self)
}

func main() {
    u := User{1, "Tom"}
    u.Test()

    mValue := u.Test
    mValue() // 隐式传递 receiver

    mExpression := (*User).Test
    mExpression(&u) // 显式传递 receiver
}   

输出结果:

    0xc42000a060, &{1 Tom}
    0xc42000a060, &{1 Tom}
    0xc42000a060, &{1 Tom}  

需要注意,method value 会复制 receiver。

package main

import "fmt"

type User struct {
    id   int
    name string
}

func (self User) Test() {
    fmt.Println(self)
}

func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。

    u.id, u.name = 2, "Jack"
    u.Test()

    mValue()
} 

输出结果

    {2 Jack}
    {1 Tom}  

面向对象

接口

接口

接口(interface)定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

接口类型

在Go语言中接口(interface)是一种类型,一种抽象的类型。

interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型

为什么要使用接口
type Cat struct{}

func (c Cat) Say() string { return "喵喵喵" }

type Dog struct{}

func (d Dog) Say() string { return "汪汪汪" }

func main() {
    c := Cat{}
    fmt.Println("猫:", c.Say())
    d := Dog{}
    fmt.Println("狗:", d.Say())
}

上面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成"能叫的动物"来处理呢?

像类似的例子在我们编程过程中会经常遇到:

比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成"支付方式"来处理呢?

比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成"图形"来处理呢?

比如销售、行政、程序员都能计算月薪,我们能不能把他们当成"员工"来处理呢?

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。

接口的定义

Go语言提倡面向接口编程。

    接口是一个或多个方法签名的集合。
    任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
    就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
    这称为Structural Typing。
    所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
    当然,该类型还可以有其他方法。

    接口只有方法声明,没有实现,没有数据字段。
    接口可以匿名嵌入其他接口,或嵌入到结构中。
    对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
    只有当接口存储的类型和对象都为nil时,接口才等于nil。
    接口调用不会做receiver的自动转换。
    接口同样支持匿名字段方法。
    接口也可实现类似OOP中的多态。
    空接口可以作为任何类型数据的容器。
    一个类型可实现多个接口。
    接口命名习惯以 er 结尾。 

每个接口由数个方法组成,接口的定义格式如下:

    type 接口类型名 interface{
        方法名1( 参数列表1 ) 返回值列表1
        方法名2( 参数列表2 ) 返回值列表2
        ...
    } 

其中:

1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。

2.方法名:当方法名首字母是大写且这个接口类型名首字母

也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。

3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

type writer interface{
    Write([]byte) error
} 

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

实现接口的条件

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个Sayer接口:

// Sayer 接口
type Sayer interface {
    say()
} 

定义dog和cat两个结构体:

type dog struct {}

type cat struct {} 

因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
} 

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。类似于java中的多态

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
} 
值接收者和指针接收者实现接口的区别

使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。

我们有一个Mover接口和一个dog结构体。

type Mover interface {
    move()
}

type dog struct {} 

值接收者实现接口

func (d dog) move() {
    fmt.Println("狗会动")
}  

此时实现接口的是dog类型:

func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
    x.move()
} 

从上面的代码中我们可以发现,使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量 。因为Go语言中有对指针类型变量求值的语法糖,dog指针fugui内部会自动求值*fugui

指针接收者实现接口

同样的代码我们再来测试一下使用指针接收者有什么区别:

Go 复制代码
func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
} 

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

类型与接口的关系

一个类型实现多个接口

一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。 例如,狗可以叫,也可以动。

多个类型实现同一接口

Go语言中不同的类型还可以实现同一接口。

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

Go 复制代码
// Sayer 接口
type Sayer interface {
    say()
}

// Mover 接口
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
} 

空接口

空接口的定义

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。类似java中的泛型

空接口的应用
空接口作为函数的参数

使用空接口实现可以接收任意类型的函数参数。

Go 复制代码
// 空接口作为函数参数
func show(a interface{}) {
    fmt.Printf("type:%T value:%v\n", a, a)
} 
空接口作为map的值

使用空接口实现可以保存任意值的字典。

Go 复制代码
// 空接口作为map值
    var studentInfo = make(map[string]interface{})
    studentInfo["name"] = "李白"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo) 
类型断言

空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。

我们来看一个具体的例子:

Go 复制代码
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil 

请看下图分解:

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

  x.(T) 

其中:

    x:表示类型为interface{}的变量
    T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。

举个例子:

Go 复制代码
func main() {
    var x interface{}
    x = "pprof.cn"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

匿名字段

go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

package main

import "fmt"

//    go支持只提供类型而不写字段名的方式,也就是匿名字段,也称为嵌入字段

//人
type Person struct {
    name string
    sex  string
    age  int
}

type Student struct {
    Person
    id   int
    addr string
}

func main() {
    // 初始化
    s1 := Student{Person{"5lmh", "man", 20}, 1, "bj"}
    fmt.Println(s1)

    s2 := Student{Person: Person{"5lmh", "man", 20}}
    fmt.Println(s2)

    s3 := Student{Person: Person{name: "5lmh"}}
    fmt.Println(s3)
}
相关推荐
dal118网工任子仪11 分钟前
66,【6】buuctf web [HarekazeCTF2019]Avatar Uploader 1
笔记·学习
02苏_16 分钟前
2025/1/21 学习Vue的第四天
学习
羊小猪~~39 分钟前
MYSQL学习笔记(四):多表关系、多表查询(交叉连接、内连接、外连接、自连接)、七种JSONS、集合
数据库·笔记·后端·sql·学习·mysql·考研
约定Da于配置41 分钟前
uniapp封装websocket
前端·javascript·vue.js·websocket·网络协议·学习·uni-app
东京老树根2 小时前
Excel 技巧15 - 在Excel中抠图头像,换背景色(★★)
笔记·学习·excel
ByteBlossom6663 小时前
MDX语言的语法糖
开发语言·后端·golang
Ronin-Lotus3 小时前
嵌入式硬件篇---ADC模拟-数字转换
笔记·stm32·单片机·嵌入式硬件·学习·低代码·模块测试
promising-w4 小时前
单片机基础模块学习——数码管
单片机·嵌入式硬件·学习
沈霁晨4 小时前
Ruby语言的Web开发
开发语言·后端·golang