01.条件语句if
if 语句:由一个布尔表达式后紧跟一个或多个语句组成
if...else 语句:if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行
if 嵌套语句:可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。
---------------------------------------------------------------------------------------------------------
if n := "abc"; x > 0 { // 初始化语句未必就是定义变量, 如 println("init") 也是可以的。
println(n[2])
} else if x < 0 { // 注意 else if 和 else 左大括号位置。
println(n[1])
} else {
println(n[0])
}
---------------------------------------------------------------------------------------------------------
不支持三元操作符(三目运算符) "a > b ? a : b"
02.switch语句
a.普通switch
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。
Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。
-----------------------------------------------------------------------------------------------------
package main
import "fmt"
func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
// switch语句也可以没有入口处的表达式
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" )
}
fmt.Printf("你的等级是 %s\n", grade )
}
b.type-switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
-----------------------------------------------------------------------------------------------------
package main
import "fmt"
func main() {
var x interface{}
//写法一:
switch i := x.(type) { // 带初始化语句
case nil:
fmt.Printf(" x 的类型 :%T\r\n", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
//写法二
var j = 0
switch j {
case 0:
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
//写法三
var k = 0
switch k {
case 0:
println("fallthrough")
fallthrough
/*
Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
而如果switch没有表达式,它会匹配true。
Go里面switch默认相当于每个case最后带有break,
匹配成功后不会自动向下执行其他case,而是跳出整个switch,
但是可以使用fallthrough强制执行后面的case代码。
*/
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
//写法三
var m = 0
switch m {
case 0, 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
//写法四
var n = 0
switch { //省略条件表达式,可当 if...else if...else
case n > 0 && n < 10:
fmt.Println("i > 0 and i < 10")
case n > 10 && n < 20:
fmt.Println("i > 10 and i < 20")
default:
fmt.Println("def")
}
}
03.select语句
a.select语句
a.介绍
select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。
如果没有case可运行,它将阻塞,直到有case可运行。
select 是Go中的一个控制结构,类似于用于通信的switch语句。
每个case必须是一个通信操作,要么是发送要么是接收。
select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
b.语法
每个case都必须是一个通信
所有channel表达式都会被求值
所有被发送的表达式都会被求值
如果任意某个通信可以进行,它就执行;其他被忽略。
如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
否则:
如果有default子句,则执行该语句。
如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
c.实例
package main
import "fmt"
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
d.select可以监听channel的数据流动
select的用法与switch语法非常类似,由select开始的一个新的选择块,每个选择条件由case语句来描述
与switch语句可以选择任何使用相等比较的条件相比,select由比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作
-------------------------------------------------------------------------------------------------
在一个select语句中,Go会按顺序从头到尾评估每一个发送和接收的语句。
如果其中的任意一个语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
①如果给出了default语句,那么就会执行default的流程,同时程序的执行会从select语句后的语句中恢复。
②如果没有default语句,那么select语句将被阻塞,直到至少有一个case可以进行下去。
e.实例
package main
import "fmt"
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
//输出:no communication
b.Golang select的使用及典型用法
a.超时判断
//比如在下面的场景中,使用全局resChan来接受response,如果时间超过3S,resChan中还没有数据返回,则第二条case将执行
var resChan = make(chan int)
// do request
func test() {
select {
case data := <-resChan:
doData(data)
case <-time.After(time.Second * 3):
fmt.Println("request time out")
}
}
func doData(data int) {
//...
}
b.退出
//主线程(协程)中如下:
var shouldQuit=make(chan struct{})
fun main(){
{
//loop
}
//...out of the loop
select {
case <-c.shouldQuit:
cleanUp()
return
default:
}
//...
}
//再另外一个协程中,如果运行遇到非法操作或不可处理的错误,就向shouldQuit发送数据通知程序停止运行
close(shouldQuit)
c.判断channel是否阻塞
//在某些情况下是存在不希望channel缓存满了的需求的,可以用如下方法判断
ch := make (chan int, 5)
//...
data:=0
select {
case ch <- data:
default:
//做相应操作,比如丢弃data。视需求而定
}
04.循环语句for
a.Go语言的For循环有3中形式,只有其中的一种使用分号
for init; condition; post { }
for condition { }
for { }
-----------------------------------------------------------------------------------------------------
init: 一般为赋值表达式,给控制变量赋初值;
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
-----------------------------------------------------------------------------------------------------
for语句执行过程如下:
①先对表达式 init 赋初值;
②判别赋值表达式 init 是否满足给定 condition 条件,若其值为真,满足循环条件,则执行循环体内语句,
然后执行 post,进入第二次循环,再判别 condition;
否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。
-----------------------------------------------------------------------------------------------------
s := "abc"
for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
println(s[i])
}
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}
for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}
b.循环嵌套
for [condition | ( init; condition; increment ) | Range]
{
for [condition | ( init; condition; increment ) | Range]
{
statement(s)
}
statement(s)
}
-----------------------------------------------------------------------------------------------------
package main
import "fmt"
func main() {
/* 定义局部变量 */
var i, j int
for i=2; i < 100; i++ {
for j=2; j <= (i/j); j++ {
if(i%j==0) {
break // 如果发现因子,则不是素数
}
}
if(j > (i/j)) {
fmt.Printf("%d 是素数\n", i)
}
}
}
c.无限循环
package main
import "fmt"
func main() {
for true {
fmt.Printf("这是无限循环。\n");
}
}
05.循环语包range
a.介绍
Golang range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。
在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
-----------------------------------------------------------------------------------------------------
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap[key] = value
}
以上代码中的 key 和 value 是可以省略。
-----------------------------------------------------------------------------------------------------
如果只想读取 key,格式如下:
for key := range oldMap
或者这样:
for key, _ := range oldMap
-----------------------------------------------------------------------------------------------------
如果只想读取 value,格式如下:
for _, value := range oldMap
b.总结
a.遍历数组(Array)
遍历数组的索引和元素值
忽略索引或忽略值
b.遍历切片(Slice)
遍历切片的索引和元素值
忽略索引或忽略值
c.遍历字符串(String)
遍历字符串中的字节
遍历字符串中的Unicode字符(rune)
忽略索引或忽略值
d.遍历字典(Map)
遍历字典的键和值
仅遍历键或值
e.遍历通道(Channel)
从通道中接收数据直到通道关闭
f.使用 range 遍历时修改数据
在遍历数组、切片或字典时修改数据
b.for 和 for range有什么区别?
a.for
遍历array和slice
遍历key为整型递增的map
遍历string
b.for range
for range可以完成所有for可以做的事情,却能做到for不能做的,包括
遍历key为string类型的map并同时获取key和value
遍历channel
c.索引参数
参数1 参数2
string index s[index] unicode, rune
array/slice index s[index]
map key m[key]
channel element
d.代码
func rangeExamples() {
s := "abc"
fmt.Println("字符串的 range 示例:")
// 使用 range 遍历字符串的索引
for i := range s {
fmt.Println(s[i])
}
fmt.Println("字符串的 range 示例 (忽略索引):")
// 使用 range 遍历字符串的字符值
for _, c := range s {
fmt.Println(c)
}
fmt.Println("字符串的 range 示例 (忽略全部返回值):")
// 使用 range 遍历字符串,仅迭代
for range s {
}
m := map[string]int{"a": 1, "b": 2}
fmt.Println("map 的 range 示例:")
// 使用 range 遍历 map 的键值对
for k, v := range m {
fmt.Println(k, v)
}
}
06.循环控制Goto、Break.Continue
1.三个语句都可以配合标签(label)使用
2.标签名区分大小写,定以后若不使用会造成编译错误
3.continue、break配合标签(label)可用于多层循环跳出
4.goto是调整执行位置,与continue、break配合标签(label)的结果并不相同
---------------------------------------------------------------------------------------------------------
// 展示 goto 的用法
func demoGoto() {
fmt.Println("Goto 示例:")
// 定义标签
fmt.Println("在 start 标签处")
goto end // 跳转到 end 标签
fmt.Println("这行不会被执行") // 被跳过
// 定义标签
end:
fmt.Println("在 end 标签处")
}
// 展示 break 和 continue 的用法
func demoBreakContinue() {
fmt.Println("Break 和 Continue 示例:")
outerLoop:
for i := 0; i < 3; i++ {
fmt.Println("外层循环:", i)
for j := 0; j < 3; j++ {
if j == 2 {
// 使用 break 跳出外层循环
break outerLoop
}
if j == 1 {
// 使用 continue 跳过本次内层循环迭代
continue
}
fmt.Println(" 内层循环:", j)
}
}
fmt.Println("外层循环已跳出")
}
// 展示 break 的使用
func demoBreak() {
fmt.Println("Break 示例:")
for i := 0; i < 5; i++ {
if i == 3 {
break // 跳出当前循环
}
fmt.Println(i)
}
fmt.Println("循环已结束")
}
// 展示 continue 的使用
func demoContinue() {
fmt.Println("Continue 示例:")
for i := 0; i < 5; i++ {
if i == 3 {
continue // 跳过本次循环迭代
}
fmt.Println(i)
}
fmt.Println("循环结束")
}
1.7 错误处理
00.汇总
在 Go 语言中,异常处理的主要方式是通过错误处理机制。
Go 不支持传统意义上的异常处理(如 try-catch 语句),而是采用显式的错误返回值来处理错误。
01.error 类型:
Go 语言通过内置的 error 类型来表示错误。error 是一个接口,定义了一个方法:
type error interface {
Error() string
}
当函数可能出现错误时,它通常会返回一个 error 类型的值。如果没有错误发生,error 为 nil。
02.return 语句:
在函数中,通常会返回一个 error 类型的值来指示操作是否成功。例如:
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
03.fmt.Errorf 函数:
fmt.Errorf 函数用于创建带有格式化信息的错误。例如:
err := fmt.Errorf("an error occurred: %v", someDetail)
04.panic 和 recover:
Go 语言提供了 panic 和 recover 机制来处理程序中的致命错误(即不期望发生的错误),但这不常用于普通的错误处理,而是用于异常情况,如程序的严重错误或不可恢复的状态。
---------------------------------------------------------------------------------------------------------
panic 用于引发一个运行时错误,它会导致程序的执行中断,直到最上层的函数返回或者程序崩溃
func riskyOperation() {
panic("something went wrong")
}
---------------------------------------------------------------------------------------------------------
recover 用于从 panic 中恢复程序的正常执行状态。它只能在 defer 的函数中调用:
func safeOperation() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
riskyOperation()
}
05.defer 关键字:
defer 关键字用于延迟执行某些操作,直到函数执行结束。
这在处理资源清理(如关闭文件、解锁)时非常有用。结合 recover 可以安全地处理异常情况。
1.8 匿名字段
01.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)
}
输出结果:
{{5lmh man 20} 1 bj}
{{5lmh man 20} 0 }
{{5lmh 0} 0 }
02.同名字段的情况
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
type Student struct {
Person
id int
addr string
//同名字段
name string
}
func main() {
var s Student
// 给自己字段赋值了
s.name = "5lmh"
fmt.Println(s)
// 若给父类同名字段赋值,如下
s.Person.name = "枯藤"
fmt.Println(s)
}
输出结果:
{{ 0} 0 5lmh}
{{枯藤 0} 0 5lmh}
03.所有的内置类型和自定义类型都是可以作为匿名字段去使用
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
// 自定义类型
type mystr string
// 学生
type Student struct {
Person
int
mystr
}
func main() {
s1 := Student{Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
}
输出结果:
{{5lmh man 18} 1 bj}
04.指针类型匿名字段
package main
import "fmt"
//人
type Person struct {
name string
sex string
age int
}
// 学生
type Student struct {
*Person
id int
addr string
}
func main() {
s1 := Student{&Person{"5lmh", "man", 18}, 1, "bj"}
fmt.Println(s1)
fmt.Println(s1.name)
fmt.Println(s1.Person.name)
}
输出结果:
{0xc00005c360 1 bj}
zs
zs
1.9 通道channel
00.介绍
a.说明
在 Go 语言中,通道(channel)是用于在 goroutine 之间进行通信和同步的核心机制。
通道支持多种操作,这些操作允许你创建、发送、接收、关闭通道等。
b.channel异常情况总结
channel:接收
nil:阻塞
非空:接收值
空的:阻塞
满了:接收值
没满:接收值
-----------------------------------------------------------------------------------------------------
channel:发送
nil:阻塞
非空:发送值
空的:发送值
满了:阻塞
没满:发送值
-----------------------------------------------------------------------------------------------------
channel:关闭
nil:panic
非空:关闭成功,读完数据后返回零值
空的:关闭成功,返回零值
满了:关闭成功,读完数据后返回零值
没满:关闭成功,读完数据后返回零值
01.创建通道
a.创建无缓冲通道
ch := make(chan int)
b.创建有缓冲通道
ch := make(chan int, 10) // 创建一个容量为10的缓冲通道
02.发送和接收数据
a.发送数据到通道
ch <- 42
b.从通道接收数据
value := <-ch
c.接收数据并处理
value, ok := <-ch
if !ok {
// 通道已关闭
}
03.关闭通道
a.关闭通道
close(ch)
b.检查通道是否关闭
value, ok := <-ch
if !ok {
// 通道已关闭
}
04.通道的发送和接收方向
a.仅发送方向通道
func sendData(ch chan<- int) {
ch <- 42
}
b.仅接收方向通道
func receiveData(ch <-chan int) {
value := <-ch
fmt.Println(value)
}
05.通道的选择(select)
使用 select 语句进行多路选择
select {
case msg1 := <-ch1:
// 处理 ch1 中的消息
case msg2 := <-ch2:
// 处理 ch2 中的消息
case ch3 <- 3:
// 发送消息到 ch3
default:
// 当没有其他通道准备好时执行
}
06.通道的缓冲区
a.创建和使用有缓冲通道
ch := make(chan int, 2)
ch <- 1
ch <- 2
// ch <- 3 // 这会阻塞,直到有接收者接收数据
b.接收缓冲区中的数据
value1 := <-ch
value2 := <-ch
07.通道的遍历
遍历通道中的数据(通常在通道关闭后)
for value := range ch {
fmt.Println(value)
}
08.通道的默认值
使用通道的默认分支
select {
case msg := <-ch:
fmt.Println("Received", msg)
default:
fmt.Println("No message received")
}
09.通道的关闭行为
关闭通道时的行为
ch := make(chan int)
go func() {
for i := 0; i < 3; i++ {
ch <- i
}
close(ch)
}()
for value := range ch {
fmt.Println(value)
}
10.无缓冲通道和有缓冲通道的区别
无缓冲通道:发送操作会阻塞,直到接收操作准备好。
有缓冲通道:发送操作只有在缓冲区满时才会阻塞,接收操作只有在缓冲区空时才会阻塞。
00.总结
在Go语言中,指针(pointer)是一个非常基础但重要的概念。指针允许你直接操作变量的内存地址,
从而可以间接地访问和修改变量的值。指针的操作包括声明、获取、解引用、修改等。
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
01.声明和初始化指针
a.声明一个指针变量
var p *int
b.声明并初始化一个指针变量
var a int = 10
var p *int = &a
02.获取指针
获取变量的地址
var a int = 10
var p *int = &a
03.解引用指针
a.访问指针指向的值
var a int = 10
var p *int = &a
value := *p // value 是 10
b.修改指针指向的值
var a int = 10
var p *int = &a
*p = 20 // a 现在是 20
04.指针算术
Go语言不支持指针算术(如 C 语言中的指针加减操作),但可以进行以下操作:
通过 uintptr 类型进行指针的整数转换
var a int = 10
var p *int = &a
addr := uintptr(unsafe.Pointer(p))
05.使用 new 和 make
a.使用 new 函数创建指针
p := new(int) // p 是一个 *int 类型的指针,指向一个初始化为零值的 int
b.使用 make 函数创建切片、映射或通道,不用于创建指针
s := make([]int, 10) // 创建一个切片,不返回指针
m := make(map[string]int) // 创建一个映射,不返回指针
ch := make(chan int) // 创建一个通道,不返回指针
06.空指针
定义和检查空指针
var p *int
if p == nil {
fmt.Println("p is nil")
}
07.指针与结构体
通过指针访问结构体字段
type Person struct {
Name string
Age int
}
func main() {
p := &Person{Name: "Alice", Age: 30}
p.Age = 31 // 通过指针修改结构体字段
fmt.Println(p.Name, p.Age)
}
08.指针与函数
a.将指针作为函数参数
func increment(n *int) {
*n++
}
func main() {
a := 10
increment(&a)
fmt.Println(a) // 输出 11
}
b.函数返回指针
func createPointer() *int {
a := 10
return &a
}
func main() {
p := createPointer()
fmt.Println(*p) // 输出 10
}
09.指针与接口
将指针赋值给接口
type Stringer interface {
String() string
}
type Person struct {
Name string
}
func (p *Person) String() string {
return p.Name
}
func main() {
p := &Person{Name: "Alice"}
var s Stringer = p
fmt.Println(s.String()) // 输出 Alice
}
2.14 指针Pointer3
00.指针
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
01.Go语言中的指针
Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。
传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
a.指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。
Go语言中使用&字符放在变量前面对变量进行“取地址”操作。
Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
-----------------------------------------------------------------------------------------------------
取变量指针的语法如下:
ptr := &v // v的类型为T
其中:
v:代表被取地址的变量,类型为T
ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。
举个例子:
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
02.指针取值
a.示例
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
func main() {
//指针取值
a := 10
b := &a // 取变量a的地址,将指针保存到b中
fmt.Printf("type of b:%T\n", b)
c := *b // 指针取值(根据指针去内存取值)
fmt.Printf("type of c:%T\n", c)
fmt.Printf("value of c:%v\n", c)
}
输出如下:
type of b:*int
type of c:int
value of c:10
总结:取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
b.变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
1.对变量进行取地址(&)操作,可以获得这个变量的指针变量。
2.指针变量的值是指针地址。
3.对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
c.指针传值示例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
03.空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil
空指针的判断
package main
import "fmt"
func main() {
var p *string
fmt.Println(p)
fmt.Printf("p的值是%s/n", p)
if p != nil {
fmt.Println("非空")
} else {
fmt.Println("空值")
}
}
04.new和make
我们先来看一个例子:
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["测试"] = 100
fmt.Println(b)
}
执行上面的代码会引发panic,为什么呢?
在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。
而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。
要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存
05.new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中,
1.Type表示类型,new函数只接受一个参数,这个参数是一个类型
2.*Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
---------------------------------------------------------------------------------------------------------
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
---------------------------------------------------------------------------------------------------------
本节开始的示例代码中var a *int只是声明了一个指针变量a但是没有初始化,
指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。
应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:
func main() {
var a *int
a = new(int)
*a = 10
fmt.Println(*a)
}
06.make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,
而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
func make(t Type, size ...IntegerType) Type
---------------------------------------------------------------------------------------------------------
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
这个我们在上一章中都有说明,关于channel我们会在后续的章节详细说明。
---------------------------------------------------------------------------------------------------------
本节开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,
需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值:
func main() {
var b map[string]int
b = make(map[string]int, 10)
b["测试"] = 100
fmt.Println(b)
}
07.new与make的区别
1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
08.指针小练习
程序定义一个int变量num的地址并打印
将num的地址赋给指针ptr,并通过ptr去修改num的值
package main
import "fmt"
func main() {
var a int
fmt.Println(&a)
var p *int
p = &a
*p = 20
fmt.Println(a)
}
2.15 结构体Struct1:替代类
00.汇总
在 Go 语言中,结构体(struct)是一种用于定义数据类型的复合数据结构,它可以包含多个字段(field),
每个字段都有一个名称和类型。结构体常用于将不同的数据组合在一起,创建复杂的数据模型。
01.定义结构体
a.定义一个简单结构体
type Person struct {
Name string
Age int
}
b.定义嵌套结构体
type Address struct {
City string
Country string
}
type Person struct {
Name string
Age int
Address Address
}
02.创建结构体实例
a.使用字段名初始化结构体
p := Person{Name: "Alice", Age: 30}
b.使用结构体字面量创建结构体
p := Person{"Alice", 30}
c.使用 new 函数创建结构体的指针
p := new(Person)
p.Name = "Alice"
p.Age = 30
03.访问和修改结构体字段
a.访问字段
fmt.Println(p.Name) // 访问 Name 字段
b.修改字段
p.Age = 31 // 修改 Age 字段
04.方法与结构体
a.为结构体定义方法
func (p *Person) Greet() string {
return "Hello, my name is " + p.Name
}
b.调用结构体的方法
fmt.Println(p.Greet())
05.结构体嵌套
a.定义嵌套结构体
type Address struct {
City string
Country string
}
type Person struct {
Name string
Age int
Address Address
}
// 创建嵌套结构体实例
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
City: "Wonderland",
Country: "Fiction",
},
}
b.访问嵌套结构体的字段
fmt.Println(p.Address.City) // 访问嵌套结构体的字段
06.结构体的比较
比较结构体是否相等
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{Name: "Alice", Age: 30}
fmt.Println(p1 == p2) // 输出: true
07.结构体标签
a.使用结构体标签
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
b.获取结构体标签
t := reflect.TypeOf(Person{})
field, _ := t.FieldByName("Name")
fmt.Println(field.Tag) // 输出: json:"name"
08.结构体与接口
将结构体赋值给接口
type Stringer interface {
String() string
}
type Person struct {
Name string
}
func (p Person) String() string {
return p.Name
}
var s Stringer = Person{Name: "Alice"}
fmt.Println(s.String()) // 输出: Alice
09.结构体的复制
复制结构体
p1 := Person{Name: "Alice", Age: 30}
p2 := p1 // 复制结构体
p2.Age = 31
fmt.Println(p1.Age) // 输出: 30
fmt.Println(p2.Age) // 输出: 31
10.结构体的零值
获取结构体的零值
var p Person
fmt.Println(p) // 输出: { 0}
2.16 结构体Struct2:替代类
01.声明
结构体的声明非常简单,例子如下:
type Person struct {
name string
age int
}
---------------------------------------------------------------------------------------------------------
结构体本身以及其内部的字段都遵守大小写命名的暴露方式。对于一些类型相同的相邻字段,可以不需要重复声明类型,如下:
type Rectangle struct {
height, width, area int
color string
}
---------------------------------------------------------------------------------------------------------
提示:在声明结构体字段时,字段名不能与方法名重复
02.实例化
a.普通创建方法
Go不存在构造方法,大多数情况下采用如下的方式来实例化结构体,初始化的时候就像map一样指定字段名称再初始化字段值
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}
-----------------------------------------------------------------------------------------------------
不过也可以省略字段名称,当省略字段名称时,就必须初始化所有字段,通常不建议使用这种方式,因为可读性很糟糕。
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}
-----------------------------------------------------------------------------------------------------
如果实例化过程比较复杂,你也可以编写一个函数来实例化结构体,就像下面这样,你也可以把它理解为一个构造函数
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
不过Go并不支持函数与方法重载,所以你无法为同一个函数或方法定义不同的参数。
如果你想以多种方式实例化结构体,要么创建多个构造函数,要么建议使用options模式。
b.选项模式
选项模式是Go语言中一种很常见的设计模式,可以更为灵活的实例化结构体,拓展性强,并且不需要改变构造函数的函数签名。假设有下面这样一个结构体
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}
-----------------------------------------------------------------------------------------------------
声明一个PersonOptions类型,它接受一个*Person类型的参数,它必须是指针,因为我们要在闭包中对Person赋值。
type PersonOptions func(p *Person)
-----------------------------------------------------------------------------------------------------
接下来创建选项函数,它们一般是With开头,它们的返回值就是一个闭包函数。
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}
-----------------------------------------------------------------------------------------------------
实际声明的构造函数签名如下,它接受一个可变长PersonOptions类型的参数。
func NewPerson(options ...PersonOptions) *Person {
// 优先应用options
p := &Person{}
for _, option := range options {
option(p)
}
// 默认值处理
if p.Age < 0 {
p.Age = 0
}
......
return p
}
-----------------------------------------------------------------------------------------------------
这样一来对于不同实例化的需求只需要一个构造函数即可完成,只需要传入不同的Options函数即可
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}
-----------------------------------------------------------------------------------------------------
函数式选项模式在很多开源项目中都能看见,gRPC Server的实例化方式也是采用了该设计模式。
函数式选项模式只适合于复杂的实例化,如果参数只有简单几个,建议还是用普通的构造函数来解决。
03.组合
在Go中,结构体之间的关系是通过组合来表示的,可以显式组合,也可以匿名组合,
后者使用起来更类似于继承,但本质上没有任何变化。例如:
---------------------------------------------------------------------------------------------------------
显式组合的方式
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}
---------------------------------------------------------------------------------------------------------
在使用时需要显式的指定字段p
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)
---------------------------------------------------------------------------------------------------------
而匿名组合可以不用显式的指定字段
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}
---------------------------------------------------------------------------------------------------------
匿名字段的名称默认为类型名,调用者可以直接访问该类型的字段和方法,但除了更加方便以外与第一种方式没有任何的区别。
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)
04.指针
对于结构体指针而言,不需要解引用就可以直接访问结构体的内容,例子如下:
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)
在编译的时候会转换为(*p).name ,(*p).age,其实还是需要解引用,不过在编码的时候可以省去,算是一种语法糖。
05.标签
结构体标签是一种元编程的形式,结合反射可以做出很多奇妙的功能,格式如下
`key1:"val1" key2:"val2"`
---------------------------------------------------------------------------------------------------------
标签是一种键值对的形式,使用空格进行分隔。结构体标签的容错性很低,如果没能按照正确的格式书写结构体,那么将会导致无法正常读取,但是在编译时却不会有任何的报错,下方是一个使用示例。
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}
结构体标签最广泛的应用就是在各种序列化格式中的别名定义,标签的使用需要结合反射才能完整发挥出其功能。
06.内存对齐
Go结构体字段的内存分布遵循内存对齐的规则,这么做可以减少CPU访问内存的次数,相应的占用的内存要多一些,属于空间换时间的一种手段。
---------------------------------------------------------------------------------------------------------
假设有如下结构体
type Num struct {
A int64
B int32
C int16
D int8
E int32
}
已知这些类型的占用字节数
int64占8个字节
int32占4个字节
int16占2字节
int8占一个字节
整个结构体的内存占用似乎是8+4+2+1+4=19个字节吗,当然不是这样,根据内存对齐规则而言,
结构体的内存占用长度至少是最大字段的整数倍,不足的则补齐。该结构体中最大的是int64占用8个字节
所以实际上是占用24个字节,其中有5个字节是无用的。
---------------------------------------------------------------------------------------------------------
再来看下面这个结构体
type Num struct {
A int8
B int64
C int8
}
明白了上面的规则后,可以很快的理解它的内存占用也是24个字节,尽管它只有三个字段,足足浪费了14个字节。
---------------------------------------------------------------------------------------------------------
但是我们可以调整字段,改成如下的顺序
type Num struct {
A int8
C int8
B int64
}
如此一来就占用的内存就变为了16字节,浪费了6个字节,减少了8个字节的内存浪费。
---------------------------------------------------------------------------------------------------------
从理论上来说,让结构体中的字段按照合理的顺序分布,可以减少其内存占用。不过实际编码过程中,
并没有必要的理由去这样做,它不一定能在减少内存占用这方面带来实质性的提升,
但一定会提高开发人员的血压和心智负担,尤其是在业务中一些结构体的字段数可能多大几十个或者数百个,所以仅做了解即可。
07.空结构体
空结构体没有字段,不占用内存空间,我们可以通过unsafe.SizeOf函数来计算占用的字节大小
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}
输出
0
空结构体的使用场景有很多,比如之前提到过的,作为map的值类型,可以将map作为set来进行使用,又或者是作为通道的类型,表示仅做通知类型的通道。
2.17 结构体Struct3:替代类
00.介绍
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
01.类型别名和自定义类型
a.自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,
Go语言中可以使用type关键字来定义自定义类型。
-----------------------------------------------------------------------------------------------------
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:
//将MyInt定义为int类型
type MyInt int
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
b.类型别名
类型别名是Go1.9版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
-----------------------------------------------------------------------------------------------------
我们之前见过的rune和byte就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
c.类型定义和类型别名的区别
类型别名与类型定义表面上看只有一个等号的差异,我们通过下面的这段代码来理解它们之间的区别。
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
02.结构体
a.介绍
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,
这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,
这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
-----------------------------------------------------------------------------------------------------
Go语言中通过struct来实现面向对象。
b.结构体的定义
使用type和struct关键字来定义结构体,具体代码格式如下:
type 类型名 struct {
字段名 字段类型
字段名 字段类型
…
}
-----------------------------------------------------------------------------------------------------
其中:
1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。
-----------------------------------------------------------------------------------------------------
举个例子,我们定义一个Person(人)结构体,代码如下:
type person struct {
name string
city string
age int8
}
-----------------------------------------------------------------------------------------------------
同样类型的字段也可以写在一行,
type person1 struct {
name, city string
age int8
}
-----------------------------------------------------------------------------------------------------
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。
这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
-----------------------------------------------------------------------------------------------------
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。
比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
c.结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
var 结构体实例 结构体类型
d.基本实例化
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "pprof.cn"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={pprof.cn 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"pprof.cn", city:"北京", age:18}
}
我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。
03.匿名结构体
a.介绍
在定义一些临时数据结构等场景下还可以使用匿名结构体。
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
}
b.创建指针类型结构体
我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}
从打印的结果中我们可以看出p2是一个结构体指针。
-----------------------------------------------------------------------------------------------------
需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。
var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}
c.取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}
p3.name = "博客"其实在底层是(*p3).name = "博客",这是Go语言帮我们实现的语法糖。
d.结构体初始化
type person struct {
name string
city string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}
e.使用键值对初始化
使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
p5 := person{
name: "pprof.cn",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}
-----------------------------------------------------------------------------------------------------
也可以对结构体指针进行键值对初始化,例如:
p6 := &person{
name: "pprof.cn",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}
-----------------------------------------------------------------------------------------------------
当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。
p7 := &person{
city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}
f.使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:
p8 := &person{
"pprof.cn",
"北京",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"pprof.cn", city:"北京", age:18}
-----------------------------------------------------------------------------------------------------
使用这种格式初始化时,需要注意:
1.必须初始化结构体的所有字段。
2.初始值的填充顺序必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用。
g.结构体内存布局
type test struct {
a int8
b int8
c int8
d int8
}
n := test{
1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
输出:
n.a 0xc0000a0060
n.b 0xc0000a0061
n.c 0xc0000a0062
n.d 0xc0000a0063
h.面试题
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "pprof.cn", age: 18},
{name: "测试", age: 23},
{name: "博客", age: 28},
}
for _, stu := range stus {
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
i.构造函数
Go语言的结构体没有构造函数,我们可以自己实现。
例如,下方的代码就实现了一个person的构造函数。
因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
-----------------------------------------------------------------------------------------------------
func newPerson(name, city string, age int8) *person {
return &person{
name: name,
city: city,
age: age,
}
}
-----------------------------------------------------------------------------------------------------
调用构造函数
p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9)
j.方法和接收者
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。
接收者的概念就类似于其他语言中的this或者 self。
-----------------------------------------------------------------------------------------------------
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
-----------------------------------------------------------------------------------------------------
其中,
1.接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
2.接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
3.方法名、参数列表、返回参数:具体格式与函数定义相同。
-----------------------------------------------------------------------------------------------------
举个例子:
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
}
方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。
k.指针类型的接收者
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,
在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。
例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。
-----------------------------------------------------------------------------------------------------
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
-----------------------------------------------------------------------------------------------------
调用该方法:
func main() {
p1 := NewPerson("测试", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
l.值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。
在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。
-----------------------------------------------------------------------------------------------------
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
p.age = newAge
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
fmt.Println(p1.age) // 25
p1.SetAge2(30) // (*p1).SetAge2(30)
fmt.Println(p1.age) // 25
}
m.什么时候应该使用指针类型接收者
1.需要修改接收者中的值
2.接收者是拷贝代价比较大的大对象
3.保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
n.任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
-----------------------------------------------------------------------------------------------------
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
o.结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
//Person 结构体Person类型
type Person struct {
string
int
}
func main() {
p1 := Person{
"pprof.cn",
18,
}
fmt.Printf("%#v\n", p1) //main.Person{string:"pprof.cn", int:18}
fmt.Println(p1.string, p1.int) //pprof.cn 18
}
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
p.嵌套结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address Address
}
func main() {
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "黑龙江",
City: "哈尔滨",
},
}
fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
q.嵌套匿名结构体
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
user2.Address.Province = "黑龙江" //通过匿名结构体.字段名访问
user2.City = "哈尔滨" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。
r.嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
//Address 地址结构体
type Address struct {
Province string
City string
CreateTime string
}
//Email 邮箱结构体
type Email struct {
Account string
CreateTime string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user3 User
user3.Name = "pprof"
user3.Gender = "女"
// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
}
s.结构体的“继承”
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
t.结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
u.结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。
同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,
键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
-----------------------------------------------------------------------------------------------------
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
v.结构体标签(Tag)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
`key1:"value1" key2:"value2"`
结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。 注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
-----------------------------------------------------------------------------------------------------
例如我们为Student结构体的每个字段定义json序列化时使用的Tag:
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "pprof",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}
w.小练习:
猜一下下列代码运行的结果是什么
package main
import "fmt"
type student struct {
id int
name string
age int
}
func demo(ce []student) {
//切片是引用传递,是可以改变值的
ce[1].age = 999
// ce = append(ce, student{3, "xiaowang", 56})
// return ce
}
func main() {
var ce []student //定义一个切片类型的结构体
ce = []student{
student{1, "xiaoming", 22},
student{2, "xiaozhang", 33},
}
fmt.Println(ce)
demo(ce)
fmt.Println(ce)
}
x.删除map类型的结构体
package main
import "fmt"
type student struct {
id int
name string
age int
}
func main() {
ce := make(map[int]student)
ce[1] = student{1, "xiaolizi", 22}
ce[2] = student{2, "wang", 23}
fmt.Println(ce)
delete(ce, 2)
fmt.Println(ce)
}
3 函数
3.1 汇总
01.常用信息1
a.总结
在Go中,函数是一等公民,函数是Go最基础的组成部分,也是Go的核心。
Go的理念便是:如果参数不一样那就是两个不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。
值得一提的是,Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值。如果你觉得在传递切片或map时会复制大量的内存,我只能告诉你大可不必担心,如果你想知道细节的话可以去实现原理章节看看。
不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。
匿名函数只能在函数内部存在,匿名函数可以简单理解为没有名称的函数,例如
闭包(Closure)这一概念,在一些语言中又被称为Lamda表达式,经常与匿名函数一起使用,函数 + 环境引用 = 闭包。看一个例子:
defer关键字描述的一个匿名函数会在函数返回之前执行。
b.声明
函数的声明格式如下
func 函数名([参数列表]) [返回值] {
函数体
}
---------------------------------------------------------------------------------------------------------
函数可以直接通过func关键字来声明,也可以声明为一个字面量,也可以作为一个类型。
// 直接声明
func DoSomething() {
}
// 字面量
var doSomthing func()
// 类型
type DoAnything func()
---------------------------------------------------------------------------------------------------------
函数签名由函数名称,参数列表,返回值组成,下面是一个完整的例子
func Sum(a, b int) int {
return a + b
}
---------------------------------------------------------------------------------------------------------
函数名称Sum,有两个int类型的参数a,b,返回值类型为int。
还有一个非常重要的点,即Go中的函数不支持重载,像下面的代码就无法通过编译
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}
---------------------------------------------------------------------------------------------------------
Go的理念便是:如果参数不一样那就是两个不同的函数,那么就不应该取一样的名字,函数重载会让代码变得混淆和难以理解。
这种理念是否正确见仁见智,至少在Go中你可以仅通过函数名就知道它是干什么的。
c.参数
Go中的参数名可以不带名称,一般这种是在接口或函数类型声明时才会用到,不过为了可读性一般还是建议尽量给参数加上名称
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}
---------------------------------------------------------------------------------------------------------
对于类型相同的参数而言,可以只需要声明一次类型,不过条件是它们必须相邻
func Log(format string, a1, a2 any) {
...
}
---------------------------------------------------------------------------------------------------------
变长参数可以接收0个或多个值,必须声明在参数列表的末尾,最典型的例子就是fmt.Printf函数。
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}
---------------------------------------------------------------------------------------------------------
值得一提的是,Go中的函数参数是传值传递,即在传递参数时会拷贝实参的值。如果你觉得在传递切片或map时会复制大量的内存,我只能告诉你大可不必担心,如果你想知道细节的话可以去实现原理章节看看。
d.返回值
下面是一个简单的函数返回值的例子,Sum函数返回一个int类型的值。
func Sum(a, b int) int {
return a + b
}
---------------------------------------------------------------------------------------------------------
当函数没有返回值时,不需要void,不带返回值即可。
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}
---------------------------------------------------------------------------------------------------------
Go允许函数有多个返回值,此时就需要用括号将返回值围起来。
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0不能作为被除数")
}
return a / b, nil
}
---------------------------------------------------------------------------------------------------------
Go也支持具名返回值,不能与参数名重复,使用具名返回值时,return关键字可以不需要指定返回哪些值。
func Sum(a, b int) (ans int) {
ans = a + b
return
}
---------------------------------------------------------------------------------------------------------
和参数一样,当有多个同类型的具名返回值时,可以省略掉重复的类型声明
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}
---------------------------------------------------------------------------------------------------------
不管具名返回值如何声明,永远都是以return关键字后的值为最高优先级。
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c,d将不会被返回
return a + b, a * b
}
e.匿名函数
匿名函数只能在函数内部存在,匿名函数可以简单理解为没有名称的函数,例如
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}
---------------------------------------------------------------------------------------------------------
或者当函数参数是一个函数类型时,这时名称不再重要,可以直接传递一个匿名函数
func main() {
DoSum(1, 2, func(a int, b int) int {
return a + b
})
}
func DoSum(a, b int, f func(int, int) int) int {
return f(a, b)
}
f.闭包
闭包(Closure)这一概念,在一些语言中又被称为Lamda表达式,经常与匿名函数一起使用,函数 + 环境引用 = 闭包。看一个例子:
func main() {
sum := Sum(1, 2)
fmt.Println(sum(3, 4))
fmt.Println(sum(5, 6))
}
func Sum(a, b int) func(int, int) int {
return func(int, int) int {
return a + b
}
}
3
3
---------------------------------------------------------------------------------------------------------
在上述代码中,无论传入什么数字,输出结果都是3,稍微修改一下代码
func main() {
sum := Sum(5)
fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2))
fmt.Println(sum(1, 2))
}
func Sum(sum int) func(int, int) int {
return func(a, b int) int {
sum += a + b
return sum
}
}
8
11
14
---------------------------------------------------------------------------------------------------------
匿名函数引用了参数sum,即便Sum函数已经执行完毕,虽然已经超出了它的生命周期,但是对其返回的函数传入参数,
依旧可以成功的修改其值,这一个过程就是闭包。事实上参数sum已经逃逸到了堆上,只要其返回值函数的生命周期没有结束,
就不会被回收掉。
---------------------------------------------------------------------------------------------------------
利用这一特性,可以非常简单的实现一个求费波那契数列的函数,代码如下
func main() {
fib := Fib(1)
for i := 0; i < 10; i++ {
fmt.Println(fib())
}
}
func Fib(n int) func() int {
if n < 1 {
n = 1
}
a, b := n, n
return func() int {
a, b = b, a+b
return a
}
}
输出为
1
2
3
5
8
13
21
34
55
89
g.延迟调用
defer关键字描述的一个匿名函数会在函数返回之前执行。
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}
2
1
---------------------------------------------------------------------------------------------------------
当有多个defer语句时,会按照后进先出的顺序执行。
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
defer func() {
fmt.Println("2")
}()
defer func() {
fmt.Println("3")
}()
defer func() {
fmt.Println("4")
}()
fmt.Println("2")
defer func() {
fmt.Println("5")
}()
}
2
5
4
3
2
1
延迟调用通常用于释放文件资源,关闭连接等操作,另一个常用的写法是用于捕获panic。
02.常用信息2
a.基本函数定义
func add(a int, b int) int {
return a + b
}
参数和返回值:函数可以接受参数并返回值。参数类型和返回类型必须明确指定。
b.多返回值
func divide(a int, b int) (int, int) {
return a / b, a % b
}
多返回值:Go 允许函数返回多个值,常用于返回结果和错误。
c.命名返回值
func swap(a, b int) (x int, y int) {
x = b
y = a
return
}
命名返回值:你可以在函数定义中为返回值命名,这样可以直接返回这些变量。
d.可变参数函数
func sum(nums ...int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
可变参数:使用 ... 语法来表示一个可变数量的参数,允许传递任意数量的参数。
e.匿名函数
func main() {
f := func(a, b int) int {
return a + b
}
fmt.Println(f(1, 2))
}
匿名函数:Go 支持匿名函数,这些函数没有名字,可以赋值给变量或直接调用。
f.闭包
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
fmt.Println(pos(1)) // 1
fmt.Println(pos(2)) // 3
fmt.Println(neg(-1)) // -1
}
闭包:匿名函数可以捕获其外部环境中的变量,这种机制称为闭包。
g.方法
type Rectangle struct {
Width, Height int
}
func (r Rectangle) Area() int {
return r.Width * r.Height
}
方法:方法是与类型绑定的函数。它们可以定义在结构体、接口或自定义类型上。
h.指针接收器方法
func (r *Rectangle) Scale(factor int) {
r.Width *= factor
r.Height *= factor
}
指针接收器方法:使用指针接收器定义的方法可以修改接收器的值。
i.递归函数
func factorial(n int) int {
if n == 0 {
return 1
}
return n * factorial(n-1)
}
递归:函数可以调用自身,常用于处理递归问题。
j.延迟执行(defer)
func main() {
defer fmt.Println("This is deferred")
fmt.Println("This is executed first")
}
defer 关键字:延迟函数的执行直到包含它的函数返回时执行,常用于资源清理。
k.错误处理
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
错误处理:Go 通过返回值的方式处理错误,而不是通过异常。
l.高阶函数
func applyOperation(a, b int, op func(int, int) int) int {
return op(a, b)
}
func add(a, b int) int {
return a + b
}
func main() {
fmt.Println(applyOperation(2, 3, add))
}
高阶函数:函数可以作为参数传递给另一个函数,也可以作为返回值返回。
m.内置函数
len 和 cap:用于获取切片、数组、通道等的长度和容量。
make:用于创建切片、映射和通道。
new:用于分配内存,并返回指向新分配的零值的指针。
copy:用于复制切片。
append:用于向切片添加元素。
panic 和 recover:用于处理运行时错误。
n.类型转换函数
var a int = 42
var b float64 = float64(a)
类型转换:将一个类型转换为另一个类型。
o.内联函数
func max(a, b int) int {
if a > b {
return a
}
return b
}
内联函数:Go 的编译器可以在某些情况下将函数内联,以提高性能(但这个过程是编译器自动进行的,开发者不能强制控制)。
p.函数的类型
type Operation func(int, int) int
func main() {
var op Operation = add
fmt.Println(op(3, 4))
}
函数类型:可以将函数类型定义为一种类型,这样可以更灵活地处理函数。
q.函数变量
func main() {
f := func(a, b int) int {
return a + b
}
fmt.Println(f(1, 2))
}
函数变量:可以将函数赋值给变量,从而在程序的其他地方调用该函数。
r.方法表达式
type MyInt int
func (m MyInt) Add(n int) int {
return int(m) + n
}
func main() {
var num MyInt = 5
f := MyInt.Add
fmt.Println(f(num, 3)) // 8
}
方法表达式:允许将方法赋值给变量,并且在调用时传递接收器。
s.接口与函数
type Adder interface {
Add(a, b int) int
}
type myAdder struct{}
func (myAdder) Add(a, b int) int {
return a + b
}
func main() {
var a Adder = myAdder{}
fmt.Println(a.Add(3, 4))
}
接口与函数:函数可以实现接口,从而使得不同类型的函数表现一致。
t.内嵌函数
func outerFunc() {
innerFunc := func() {
fmt.Println("Hello from inner function")
}
innerFunc()
}
内嵌函数:在函数内部定义另一个函数,可以将相关的功能封装在一起。
u.包级函数
// utils.go
package utils
func Add(a, b int) int {
return a + b
}
// main.go
package main
import (
"fmt"
"path/to/your/project/utils"
)
func main() {
fmt.Println(utils.Add(1, 2))
}
包级函数:函数可以定义在包中并被导出(首字母大写),以便在其他包中使用。
v.尾递归优化
Go 的编译器不支持尾递归优化,但你可以通过重写递归函数为循环来优化性能。
func factorial(n int) int {
result := 1
for n > 0 {
result *= n
n--
}
return result
}
w.延迟执行多个函数
func main() {
defer fmt.Println("First deferred")
defer fmt.Println("Second deferred")
defer fmt.Println("Third deferred")
}
多个延迟函数:延迟执行的函数以后进先出(LIFO)的顺序执行。
x.Panic 和 Recover
func mayPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from:", r)
}
}()
panic("Something went wrong")
}
func main() {
mayPanic()
fmt.Println("Continuing execution")
}
Panic 和 Recover:panic 用于引发运行时错误,recover 用于从 panic 状态中恢复。
y.函数返回多个值及其应用
func divide(a, b int) (int, int, error) {
if b == 0 {
return 0, 0, fmt.Errorf("division by zero")
}
return a / b, a % b, nil
}
func main() {
quotient, remainder, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Quotient:", quotient, "Remainder:", remainder)
}
}
多个返回值及其应用:Go 的多返回值机制非常适合处理需要返回多个结果或错误的场景。
3.2 函数定义
01.golang函数特点:
无需声明原型。
支持不定 变参。
支持多返回值。
支持命名返回参数。
支持匿名函数和闭包。
函数也是一种类型,一个函数可以赋值给变量。
---------------------------------------------------------------------------------------------------------
不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
不支持 重载 (overload)
不支持 默认参数 (default parameter)。
02.函数声明:
函数声明包含一个函数名,参数列表,返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。
函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。
函数可以没有参数或接受多个参数。
注意类型在变量名之后 。
当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
函数可以返回任意数量的返回值。
使用关键字 func 定义函数,左大括号依旧不能另起一行。
---------------------------------------------------------------------------------------------------------
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}
---------------------------------------------------------------------------------------------------------
函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。
package main
import "fmt"
func test(fn func() int) int {
return fn()
}
// 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s1, s2)
}
输出结果:
100 10, 20
---------------------------------------------------------------------------------------------------------
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数标识符。
package math
func Sin(x float64) float //implemented in assembly language
3.3 参数
01.函数参数
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。
但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
---------------------------------------------------------------------------------------------------------
值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
func swap(x, y int) int {
... ...
}
---------------------------------------------------------------------------------------------------------
引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
package main
import (
"fmt"
)
/* 定义相互交换值的函数 */
func swap(x, y *int) {
var temp int
temp = *x /* 保存 x 的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y*/
}
func main() {
var a, b int = 1, 2
/*
调用 swap() 函数
&a 指向 a 指针,a 变量的地址
&b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Println(a, b)
}
输出结果:
2 1
02.函数参数
a.介绍
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
注意2:map、slice、chan、指针、interface默认以引用的方式传递。
不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
b.示例
在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。
func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}
func add(a int, b int, args…int) int { //2个或多个参数
}
注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.
c.示例
任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。
用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
func myfunc(args ...interface{}) {
}
-----------------------------------------------------------------------------------------------------
代码:
package main
import (
"fmt"
)
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
println(test("sum: %d", 1, 2, 3))
}
输出结果:
sum: 6
-----------------------------------------------------------------------------------------------------
使用 slice 对象做变参时,必须展开。(slice...)
package main
import (
"fmt"
)
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
s := []int{1, 2, 3}
res := test("sum: %d", s...) // slice... 展开slice
println(res)
}
3.4 返回值
01.函数返回值
"_"标识符,用来忽略函数的某个返回值
Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。
返回值的名称应当具有一定的意义,可以作为文档使用。
没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。
直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
)
func add(a, b int) (c int) {
c = a + b
return
}
func calc(a, b int) (sum int, avg int) {
sum = a + b
avg = (a + b) / 2
return
}
func main() {
var a, b int = 1, 2
c := add(a, b)
sum, avg := calc(a, b)
fmt.Println(a, b, c, sum, avg)
}
输出结果:
1 2 3 3 1
02.示例
Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。
package main
func test() (int, int) {
return 1, 2
}
func main() {
// s := make([]int, 2)
// s = test() // Error: multiple-value test() in single-value context
x, _ := test()
println(x)
}
输出结果:
1
---------------------------------------------------------------------------------------------------------
多返回值可直接作为其他函数调用实参。
package main
func test() (int, int) {
return 1, 2
}
func add(x, y int) int {
return x + y
}
func sum(n ...int) int {
var x int
for _, i := range n {
x += i
}
return x
}
func main() {
println(add(test()))
println(sum(test()))
}
输出结果:
3
3
---------------------------------------------------------------------------------------------------------
命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。
package main
func add(x, y int) (z int) {
z = x + y
return
}
func main() {
println(add(1, 2))
}
输出结果:
3
---------------------------------------------------------------------------------------------------------
命名返回参数可被同名局部变量遮蔽,此时需要显式返回。
func add(x, y int) (z int) {
{ // 不能在一个级别,引发 "z redeclared in this block" 错误。
var z = x + y
// return // Error: z is shadowed during return
return z // 必须显式返回。
}
}
---------------------------------------------------------------------------------------------------------
命名返回参数允许 defer 延迟调用通过闭包读取和修改。
package main
func add(x, y int) (z int) {
defer func() {
z += 100
}()
z = x + y
return
}
func main() {
println(add(1, 2))
}
输出结果:
103
---------------------------------------------------------------------------------------------------------
显式 return 返回前,会先修改命名返回参数。
package main
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
}
func main() {
println(add(1, 2)) // 输出: 203
}
输出结果:
203
203
3.5 匿名函数
01.匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式。1958年LISP首先采用匿名函数。
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
"math"
)
func main() {
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}
输出结果:
2
---------------------------------------------------------------------------------------------------------
上面先定义了一个名为getSqrt 的变量,初始化该变量时和之前的变量初始化有些不同,使用了func,func是定义函数的,
可是这个函数和上面说的函数最大不同就是没有函数名,也就是匿名函数。这里将一个函数当做一个变量一样的操作。
02.匿名函数
Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。
---------------------------------------------------------------------------------------------------------
package main
func main() {
// --- function variable ---
fn := func() { println("Hello, World!") }
fn()
// --- function collection ---
fns := [](func(x int) int){
func(x int) int { return x + 1 },
func(x int) int { return x + 2 },
}
println(fns[0](100))
// --- function as field ---
d := struct {
fn func() string
}{
fn: func() string { return "Hello, World!" },
}
println(d.fn())
// --- channel of function ---
fc := make(chan func() string, 2)
fc <- func() string { return "Hello, World!" }
println((<-fc)())
}
输出结果:
Hello, World!
101
Hello, World!
Hello, World!
3.6 闭包、递归
00.闭包详解
闭包的应该都听过,但到底什么是闭包呢?
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
维基百科讲,闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
看着上面的描述,会发现闭包和匿名函数似乎有些像。可是可能还是有些云里雾里的。因为跳过闭包的创建过程直接理解闭包的定义是非常困难的。目前在JavaScript、Go、PHP、Scala、Scheme、Common Lisp、Smalltalk、Groovy、Ruby、 Python、Lua、objective c、Swift 以及Java8以上等语言中都能找到对闭包不同程度的支持。通过支持闭包的语法可以发现一个特点,他们都有垃圾回收(GC)机制。 javascript应该是普及度比较高的编程语言了,通过这个来举例应该好理解写。看下面的代码,只要关注script里方法的定义和调用就可以了。
---------------------------------------------------------------------------------------------------------
<!DOCTYPE html>
<html lang="zh">
<head>
<title></title>
</head>
<body>
</body>
</html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.2.6/jquery.min.js" type="text/javascript"></script>
<script>
function a(){
var i=0;
function b(){
console.log(++i);
document.write("<h1>"+i+"</h1>");
}
return b;
}
$(function(){
var c=a();
c();
c();
c();
//a(); //不会有信息输出
document.write("<h1>=============</h1>");
var c2=a();
c2();
c2();
});
</script>
---------------------------------------------------------------------------------------------------------
这段代码有两个特点:
函数b嵌套在函数a内部 函数a返回函数b 这样在执行完var c=a()后,变量c实际上是指向了函数b(),再执行函数c()后就会显示i的值,第一次为1,第二次为2,第三次为3,以此类推。 其实,这段代码就创建了一个闭包。因为函数a()外的变量c引用了函数a()内的函数b(),就是说:
当函数a()的内部函数b()被函数a()外的一个变量引用的时候,就创建了一个闭包。 在上面的例子中,由于闭包的存在使得函数a()返回后,a中的i始终存在,这样每次执行c(),i都是自加1后的值。 从上面可以看出闭包的作用就是在a()执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a()所占用的资源,因为a()的内部函数b()的执行需要依赖a()中的变量i。
在给定函数被多次调用的过程中,这些私有变量能够保持其持久性。变量的作用域仅限于包含它们的函数,因此无法从其它程序代码部分进行访问。不过,变量的生存期是可以很长,在一次函数调用期间所创建所生成的值在下次函数调用时仍然存在。正因为这一特点,闭包可以用来完成信息隐藏,并进而应用于需要状态表达的某些编程范型中。 下面来想象另一种情况,如果a()返回的不是函数b(),情况就完全不同了。因为a()执行完后,b()没有被返回给a()的外界,只是被a()所引用,而此时a()也只会被b()引 用,因此函数a()和b()互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。所以直接调用a();是页面并没有信息输出。
下面来说闭包的另一要素引用环境。c()跟c2()引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数a()每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。这和c()和c()的调用顺序都是无关的。
01.Go的闭包
Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。
下面我来将之前的JavaScript的闭包例子用Go来实现。
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
)
func a() func() int {
i := 0
b := func() int {
i++
fmt.Println(i)
return i
}
return b
}
func main() {
c := a()
c()
c()
c()
a() //不会输出i
}
输出结果:
1
2
3
可以发现,输出和之前的JavaScript的代码是一致的。具体的原因和上面的也是一样的,这说明Go语言是支持闭包的。
---------------------------------------------------------------------------------------------------------
闭包复制的是原对象指针,这就很容易解释延迟引用现象。
package main
import "fmt"
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
f := test()
f()
}
输出:
x (0xc42007c008) = 100
x (0xc42007c008) = 100
---------------------------------------------------------------------------------------------------------
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。
当调 匿名函数时,只需以某个寄存器传递该对象即可。
FuncVal { func_address, closure_var_pointer ... }
---------------------------------------------------------------------------------------------------------
外部引用函数参数局部变量
package main
import "fmt"
// 外部引用函数参数局部变量
func add(base int) func(int) int {
return func(i int) int {
base += i
return base
}
}
func main() {
tmp1 := add(10)
fmt.Println(tmp1(1), tmp1(2))
// 此时tmp1和tmp2不是一个实体了
tmp2 := add(100)
fmt.Println(tmp2(1), tmp2(2))
}
---------------------------------------------------------------------------------------------------------
返回2个闭包
package main
import "fmt"
// 返回2个函数类型的返回值
func test01(base int) (func(int) int, func(int) int) {
// 定义2个函数,并返回
// 相加
add := func(i int) int {
base += i
return base
}
// 相减
sub := func(i int) int {
base -= i
return base
}
// 返回
return add, sub
}
func main() {
f1, f2 := test01(10)
// base一直是没有消
fmt.Println(f1(1), f2(2))
// 此时base是9
fmt.Println(f1(3), f2(4))
}
02.Go语言递归函数
递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。
构成递归需具备的条件:
1.子问题须与原始问题为同样的事,且更为简单。
2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
---------------------------------------------------------------------------------------------------------
数字阶乘
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且0的阶乘为1。自然数n的阶乘写作n!。1808年,基斯顿·卡曼引进这个表示法。
package main
import "fmt"
func factorial(i int) int {
if i <= 1 {
return 1
}
return i * factorial(i-1)
}
func main() {
var i int = 7
fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}
输出结果:
Factorial of 7 is 5040
---------------------------------------------------------------------------------------------------------
斐波那契数列(Fibonacci)
这个数列从第3项开始,每一项都等于前两项之和。
package main
import "fmt"
func fibonaci(i int) int {
if i == 0 {
return 0
}
if i == 1 {
return 1
}
return fibonaci(i-1) + fibonaci(i-2)
}
func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\n", fibonaci(i))
}
}
输出结果:
0
1
1
2
3
5
8
13
21
34
3.7 延迟调用
01.Golang延迟调用:
defer特性:
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
defer用途:
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
---------------------------------------------------------------------------------------------------------
go语言 defer
go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。
defer 是先进后出
这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
---------------------------------------------------------------------------------------------------------
package main
import "fmt"
func main() {
var whatever [5]struct{}
for i := range whatever {
defer fmt.Println(i)
}
}
输出结果:
4
3
2
1
0
---------------------------------------------------------------------------------------------------------
defer 碰上闭包
package main
import "fmt"
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
}
输出结果:
4
4
4
4
4
其实go说的很清楚,我们一起来看看go spec如何说的
Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
---------------------------------------------------------------------------------------------------------
defer f.Close
这个大家用的都很频繁,但是go语言编程举了一个可能一不小心会犯错的例子.
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer t.Close()
}
}
输出结果:
c closed
c closed
c closed
这个输出并不会像我们预计的输出c b a,而是输出c c c
可是按照前面的go spec中的说明,应该输出c b a才对啊.
---------------------------------------------------------------------------------------------------------
那我们换一种方式来调用一下.
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func Close(t Test) {
t.Close()
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
defer Close(t)
}
}
输出结果:
c closed
b closed
a closed
这个时候输出的就是c b a
---------------------------------------------------------------------------------------------------------
当然,如果你不想多写一个函数,也很简单,可以像下面这样,同样会输出c b a
看似多此一举的声明
package main
import "fmt"
type Test struct {
name string
}
func (t *Test) Close() {
fmt.Println(t.name, " closed")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
t2 := t
defer t2.Close()
}
}
输出结果:
c closed
b closed
a closed
通过以上例子,结合Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.这句话。可以得出下面的结论:
defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是并没有说struct这里的this指针如何处理,通过这个例子可以看出go语言并没有把这个明确写出来的this指针当作参数来看待。
---------------------------------------------------------------------------------------------------------
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package main
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}
输出结果:
c
b
a
panic: runtime error: integer divide by zero
---------------------------------------------------------------------------------------------------------
*延迟调用参数在注册时求值或复制,可用指针或闭包 "延迟" 读取。
package main
func test() {
x, y := 10, 20
defer func(i int) {
println("defer:", i, y) // y 闭包引用
}(x) // x 被复制
x += 10
y += 100
println("x =", x, "y =", y)
}
func main() {
test()
}
输出结果:
x = 20 y = 120
defer: 10 120
---------------------------------------------------------------------------------------------------------
*滥用 defer 可能会导致性能问题,尤其是在一个 "大循环" 里。
package main
import (
"fmt"
"sync"
"time"
)
var lock sync.Mutex
func test() {
lock.Lock()
lock.Unlock()
}
func testdefer() {
lock.Lock()
defer lock.Unlock()
}
func main() {
func() {
t1 := time.Now()
for i := 0; i < 10000; i++ {
test()
}
elapsed := time.Since(t1)
fmt.Println("test elapsed: ", elapsed)
}()
func() {
t1 := time.Now()
for i := 0; i < 10000; i++ {
testdefer()
}
elapsed := time.Since(t1)
fmt.Println("testdefer elapsed: ", elapsed)
}()
}
输出结果:
test elapsed: 223.162µs
testdefer elapsed: 781.304µs
02.defer陷阱
defer 与 closure
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>
解释:如果 defer 后面跟的不是一个 closure 最后执行的时候我们得到的并不是最新的值。
---------------------------------------------------------------------------------------------------------
defer 与 return
package main
import "fmt"
func foo() (i int) {
i = 0
defer func() {
fmt.Println(i)
}()
return 2
}
func main() {
foo()
}
输出结果:
2
解释:在有具名返回值的函数中(这里具名返回值为 i),执行 return 2 的时候实际上已经将 i 的值重新赋值为 2。所以defer closure 输出结果为 2 而不是 1。
---------------------------------------------------------------------------------------------------------
defer nil 函数
package main
import (
"fmt"
)
func test() {
var run func() = nil
defer run()
fmt.Println("runs")
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
test()
}
输出结果:
runs
runtime error: invalid memory address or nil pointer dereference
解释:名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。然而值得注意的是,run() 的声明是没有问题,因为在test函数运行完成后它才会被调用。
---------------------------------------------------------------------------------------------------------
在错误的位置使用 defer
当 http.Get 失败时会抛出异常。
package main
import "net/http"
func do() error {
res, err := http.Get("http://www.google.com")
defer res.Body.Close()
if err != nil {
return err
}
// ..code...
return nil
}
func main() {
do()
}
输出结果:
panic: runtime error: invalid memory address or nil pointer dereference
因为在这里我们并没有检查我们的请求是否成功执行,当它失败的时候,我们访问了 Body 中的空变量 res ,因此会抛出异常
---------------------------------------------------------------------------------------------------------
解决方案
总是在一次成功的资源分配下面使用 defer ,对于这种情况来说意味着:当且仅当 http.Get 成功执行时才使用 defer
package main
import "net/http"
func do() error {
res, err := http.Get("http://xxxxxxxxxx")
if res != nil {
defer res.Body.Close()
}
if err != nil {
return err
}
// ..code...
return nil
}
func main() {
do()
}
在上述的代码中,当有错误的时候,err 会被返回,否则当整个函数返回的时候,会关闭 res.Body 。
解释:在这里,你同样需要检查 res 的值是否为 nil ,这是 http.Get 中的一个警告。通常情况下,出错的时候,返回的内容应为空并且错误会被返回,可当你获得的是一个重定向 error 时, res 的值并不会为 nil ,但其又会将错误返回。上面的代码保证了无论如何 Body 都会被关闭,如果你没有打算使用其中的数据,那么你还需要丢弃已经接收的数据。
-------------------------------------------------------------------------------------------------------------
不检查错误
在这里,f.Close() 可能会返回一个错误,可这个错误会被我们忽略掉
package main
import "os"
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer f.Close()
}
// ..code...
return nil
}
func main() {
do()
}
---------------------------------------------------------------------------------------------------------
改进一下
package main
import "os"
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if err := f.Close(); err != nil {
// log etc
}
}()
}
// ..code...
return nil
}
func main() {
do()
}
---------------------------------------------------------------------------------------------------------
再改进一下
通过命名的返回变量来返回 defer 内的错误。
package main
import "os"
func do() (err error) {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if ferr := f.Close(); ferr != nil {
err = ferr
}
}()
}
// ..code...
return nil
}
func main() {
do()
}
---------------------------------------------------------------------------------------------------------
释放相同的资源
如果你尝试使用相同的变量释放不同的资源,那么这个操作可能无法正常执行。
package main
import (
"fmt"
"os"
)
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("defer close book.txt err %v\n", err)
}
}()
}
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
if f != nil {
defer func() {
if err := f.Close(); err != nil {
fmt.Printf("defer close another-book.txt err %v\n", err)
}
}()
}
return nil
}
func main() {
do()
}
输出结果: defer close book.txt err close ./another-book.txt: file already closed
---------------------------------------------------------------------------------------------------------
当延迟函数执行时,只有最后一个变量会被用到,因此,f 变量 会成为最后那个资源 (another-book.txt)。而且两个 defer 都会将这个资源作为最后的资源来关闭
解决方案:
package main
import (
"fmt"
"io"
"os"
)
func do() error {
f, err := os.Open("book.txt")
if err != nil {
return err
}
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close book.txt err %v\n", err)
}
}(f)
}
// ..code...
f, err = os.Open("another-book.txt")
if err != nil {
return err
}
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close another-book.txt err %v\n", err)
}
}(f)
}
return nil
}
func main() {
do()
}
01.go test工具
Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的,并不需要学习新的语法、规则或工具。
go test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内,所有以_test.go为后缀名的源代码文件都是go test测试的一部分,不会被go build编译到最终的可执行文件中。
---------------------------------------------------------------------------------------------------------
在*_test.go文件中有三种类型的函数,单元测试函数、基准测试函数和示例函数。
类型 格式 作用
测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确
基准函数 函数名前缀为Benchmark 测试函数的性能
示例函数 函数名前缀为Example 为文档提供示例文档
go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
---------------------------------------------------------------------------------------------------------
Golang单元测试对文件名和方法名,参数都有很严格的要求。
1、文件名必须以xx_test.go命名
2、方法必须是Test[^a-z]开头
3、方法参数必须 t *testing.T
4、使用go test执行单元测试
---------------------------------------------------------------------------------------------------------
go test的参数解读:
go test是go语言自带的测试工具,其中包含的是两类,单元测试和性能测试
通过go help test可以看到go test的使用说明:
格式形如: go test [-c] [-i] [build flags] [packages] [flags for test binary]
参数解读:
-c : 编译go test成为可执行的二进制文件,但是不运行测试。
-i : 安装测试包依赖的package,但是不运行测试。
---------------------------------------------------------------------------------------------------------
关于build flags,调用go help build,这些是编译运行过程中需要使用到的参数,一般设置为空
关于packages,调用go help packages,这些是关于包的管理,一般设置为空
关于flags for test binary,调用go help testflag,这些是go test过程中经常使用到的参数
---------------------------------------------------------------------------------------------------------
-test.v : 是否输出全部的单元测试用例(不管成功或者失败),默认没有加上,所以只输出失败的单元测试用例。
-test.run pattern: 只跑哪些单元测试用例
-test.bench patten: 只跑那些性能测试用例
-test.benchmem : 是否在性能测试的时候输出内存情况
-test.benchtime t : 性能测试运行的时间,默认是1s
-test.cpuprofile cpu.out : 是否输出cpu性能分析文件
-test.memprofile mem.out : 是否输出内存性能分析文件
-test.blockprofile block.out : 是否输出内部goroutine阻塞的性能分析文件
-test.memprofilerate n : 内存性能分析的时候有一个分配了多少的时候才打点记录的问题。这个参数就是设置打点的内存分配间隔,也就是profile中一个sample代表的内存大小。默认是设置为512 * 1024的。如果你将它设置为1,则每分配一个内存块就会在profile中有个打点,那么生成的profile的sample就会非常多。如果你设置为0,那就是不做打点了。你可以通过设置memprofilerate=1和GOGC=off来关闭内存回收,并且对每个内存块的分配进行观察。
-test.blockprofilerate n: 基本同上,控制的是goroutine阻塞时候打点的纳秒数。默认不设置就相当于-test.blockprofilerate=1,每一纳秒都打点记录一下
-test.parallel n : 性能测试的程序并行cpu数,默认等于GOMAXPROCS。
-test.timeout t : 如果测试用例运行时间超过t,则抛出panic
-test.cpu 1,2,4 : 程序运行在哪些CPU上面,使用二进制的1所在位代表,和nginx的nginx_worker_cpu_affinity是一个道理
-test.short : 将那些运行时间较长的测试用例运行时间缩短
---------------------------------------------------------------------------------------------------------
目录结构:
test
|
—— calc.go
|
—— calc_test.go
02.测试函数的格式
每个测试函数必须导入testing包,测试函数的基本格式(签名)如下:
func TestName(t *testing.T){
// ...
}
---------------------------------------------------------------------------------------------------------
测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头,举几个例子:\
func TestAdd(t *testing.T){ ... }
func TestSum(t *testing.T){ ... }
func TestLog(t *testing.T){ ... }
---------------------------------------------------------------------------------------------------------1
其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下:
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (t *T) Parallel()
func (t *T) Run(name string, f func(t *T)) bool
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
03.测试函数示例
就像细胞是构成我们身体的基本单位,一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序,它会将结果与预期输出进行比较。
接下来,我们定义一个split的包,包中定义了一个Split函数,具体实现如下:
---------------------------------------------------------------------------------------------------------
// split/split.go
package split
import "strings"
// split package with a single split function.
// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
i := strings.Index(s, sep)
for i > -1 {
result = append(result, s[:i])
s = s[i+1:]
i = strings.Index(s, sep)
}
result = append(result, s)
return
}
---------------------------------------------------------------------------------------------------------
在当前目录下,我们创建一个split_test.go的测试文件,并定义一个测试函数如下:
// split/split_test.go
package split
import (
"reflect"
"testing"
)
func TestSplit(t *testing.T) { // 测试函数名必须以Test开头,必须接收一个*testing.T类型参数
got := Split("a:b:c", ":") // 程序输出的结果
want := []string{"a", "b", "c"} // 期望的结果
if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
t.Errorf("excepted:%v, got:%v", want, got) // 测试失败输出错误提示
}
}
---------------------------------------------------------------------------------------------------------
此时split这个包中的文件如下:
split $ ls -l
total 16
-rw-r--r-- 1 pprof staff 408 4 29 15:50 split.go
-rw-r--r-- 1 pprof staff 466 4 29 16:04 split_test.go
---------------------------------------------------------------------------------------------------------
在split包路径下,执行go test命令,可以看到输出结果如下:
split $ go test
PASS
ok github.com/pprof/studygo/code_demo/test_demo/split 0.005s
---------------------------------------------------------------------------------------------------------
一个测试用例有点单薄,我们再编写一个测试使用多个字符切割字符串的例子,在split_test.go中添加如下测试函数:
func TestMoreSplit(t *testing.T) {
got := Split("abcd", "bc")
want := []string{"a", "d"}
if !reflect.DeepEqual(want, got) {
t.Errorf("excepted:%v, got:%v", want, got)
}
}
---------------------------------------------------------------------------------------------------------
再次运行go test命令,输出结果如下:
split $ go test
--- FAIL: TestMultiSplit (0.00s)
split_test.go:20: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/pprof/studygo/code_demo/test_demo/split 0.006s
---------------------------------------------------------------------------------------------------------
这一次,我们的测试失败了。我们可以为go test命令添加-v参数,查看测试函数名称和运行时间:
split $ go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/pprof/studygo/code_demo/test_demo/split 0.005s
---------------------------------------------------------------------------------------------------------
这一次我们能清楚的看到是TestMoreSplit这个测试没有成功。 还可以在go test命令后添加-run参数,它对应一个正则表达式,只有函数名匹配上的测试函数才会被go test命令执行。
split $ go test -v -run="More"
=== RUN TestMoreSplit
--- FAIL: TestMoreSplit (0.00s)
split_test.go:21: excepted:[a d], got:[a cd]
FAIL
exit status 1
FAIL github.com/pprof/studygo/code_demo/test_demo/split 0.006s
---------------------------------------------------------------------------------------------------------
现在我们回过头来解决我们程序中的问题。很显然我们最初的split函数并没有考虑到sep为多个字符的情况,我们来修复下这个Bug:
package split
import "strings"
// split package with a single split function.
// Split slices s into all substrings separated by sep and
// returns a slice of the substrings between those separators.
func Split(s, sep string) (result []string) {
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
}
---------------------------------------------------------------------------------------------------------
这一次我们再来测试一下,我们的程序。注意,当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数,我们应该完整的运行所有的测试,保证不会因为修改代码而引入了新的问题。
split $ go test -v
=== RUN TestSplit
--- PASS: TestSplit (0.00s)
=== RUN TestMoreSplit
--- PASS: TestMoreSplit (0.00s)
PASS
ok github.com/pprof/studygo/code_demo/test_demo/split 0.006s
这一次我们的测试都通过
3.10 压力测试
01.Go怎么写测试用例
开发程序其中很重要的一点是测试,我们如何保证代码的质量,如何保证每个函数是可运行,运行结果是正确的,又如何保证写出来的代码性能是好的,我们知道单元测试的重点在于发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决,而性能测试的重点在于发现程序设计上的一些问题,让线上的程序能够在高并发的情况下还能保持稳定。本小节将带着这一连串的问题来讲解Go语言中如何来实现单元测试和性能测试。
Go语言中自带有一个轻量级的测试框架testing和自带的go test命令来实现单元测试和性能测试,testing框架和其他语言中的测试框架类似,你可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例,那么接下来让我们一一来看一下怎么写。
另外建议安装gotests插件自动生成测试代码:
go get -u -v github.com/cweill/gotests/...
02.如何编写测试用例
由于go test命令只能在一个相应的目录下执行所有文件,所以我们接下来新建一个项目目录gotest,这样我们所有的代码和测试代码都在这个目录下。
接下来我们在该目录下面创建两个文件:gotest.go和gotest_test.go
---------------------------------------------------------------------------------------------------------
gotest.go:这个文件里面我们是创建了一个包,里面有一个函数实现了除法运算:
package gotest
import (
"errors"
)
func Division(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
---------------------------------------------------------------------------------------------------------
gotest_test.go:这是我们的单元测试文件,但是记住下面的这些原则:
文件名必须是_test.go结尾的,这样在执行go test的时候才会执行到相应的代码
你必须import testing这个包
所有的测试用例函数必须是Test开头
测试用例会按照源代码中写的顺序依次执行
测试函数TestXxx()的参数是testing.T,我们可以使用该类型来记录错误或者是测试状态
测试格式:func TestXxx (t *testing.T),Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],例如Testintdiv是错误的函数名。
函数中通过调用testing.T的Error, Errorf, FailNow, Fatal, FatalIf方法,说明测试不通过,调用Log方法用来记录测试的信息。
---------------------------------------------------------------------------------------------------------
下面是我们的测试用例的代码:
package gotest
import (
"testing"
)
func Test_Division_1(t *testing.T) {
if i, e := Division(6, 2); i != 3 || e != nil { //try a unit test on function
t.Error("除法函数测试没通过") // 如果不是如预期的那么就报错
} else {
t.Log("第一个测试通过了") //记录一些你期望记录的信息
}
}
func Test_Division_2(t *testing.T) {
t.Error("就是不通过")
}
---------------------------------------------------------------------------------------------------------
我们在项目目录下面执行go test,就会显示如下信息:
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL gotest 0.013s
---------------------------------------------------------------------------------------------------------
从这个结果显示测试没有通过,因为在第二个测试函数中我们写死了测试不通过的代码t.Error,那么我们的第一个函数执行的情况怎么样呢?默认情况下执行go test是不会显示测试通过的信息的,我们需要带上参数go test -v,这样就会显示如下信息:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- FAIL: Test_Division_2 (0.00 seconds)
gotest_test.go:16: 就是不通过
FAIL
exit status 1
FAIL gotest 0.012s
---------------------------------------------------------------------------------------------------------
上面的输出详细的展示了这个测试的过程,我们看到测试函数1Test_Division_1测试通过,而测试函数2Test_Division_2测试失败了,最后得出结论测试不通过。接下来我们把测试函数2修改成如下代码:
func Test_Division_2(t *testing.T) {
if _, e := Division(6, 0); e == nil { //try a unit test on function
t.Error("Division did not work as expected.") // 如果不是如预期的那么就报错
} else {
t.Log("one test passed.", e) //记录一些你期望记录的信息
}
}
---------------------------------------------------------------------------------------------------------
然后我们执行go test -v,就显示如下信息,测试通过了:
=== RUN Test_Division_1
--- PASS: Test_Division_1 (0.00 seconds)
gotest_test.go:11: 第一个测试通过了
=== RUN Test_Division_2
--- PASS: Test_Division_2 (0.00 seconds)
gotest_test.go:20: one test passed. 除数不能为0
PASS
ok gotest 0.013s
03.如何编写压力测试
压力测试用来检测函数(方法)的性能,和编写单元功能测试的方法类似,此处不再赘述,但需要注意以下几点:
压力测试用例必须遵循如下格式,其中XXX可以是任意字母数字的组合,但是首字母不能是小写字母
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,代码如下所示:
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=".*",可以看到如下结果:
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纳秒。最后一条显示总共的执行时间。
4 方法
4.1 汇总
01.常用信息1
a.方法与函数的区别在于,方法拥有接收者,而函数没有,且只有自定义类型能够拥有方法。
先来看一个例子。
type IntSlice []int
func (i IntSlice) Get(index int) int {
return i[index]
}
func (i IntSlice) Set(index, val int) {
i[index] = val
}
func (i IntSlice) Len() int {
return len(i)
}
先声明了一个类型IntSlice,其底层类型为[]int,再声明了三个方法Get,Set和Len,
方法的长相与函数并无太大的区别,只是多了一小段(i IntSlice) 。
i就是接收者,IntSlice就是接收者的类型,接收者就类似于其他语言中的this或self,只不过在Go中需要显示的指明。
---------------------------------------------------------------------------------------------------------
func main() {
var intSlice IntSlice
intSlice = []int{1, 2, 3, 4, 5}
fmt.Println(intSlice.Get(0))
intSlice.Set(0, 2)
fmt.Println(intSlice)
fmt.Println(intSlice.Len())
}
方法的使用就类似于调用一个类的成员方法,先声明,再初始化,再调用。
b.值接收者
接收者也分两种类型,值接收者和指针接收者,先看一个例子
type MyInt int
func (i MyInt) Set(val int) {
i = MyInt(val) // 修改了,但是不会造成任何影响
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}
---------------------------------------------------------------------------------------------------------
上述代码运行过后,会发现myInt的值依旧是1,并没有被修改成2。方法在被调用时,会将接收者的值传入方法中,
上例的接收者就是一个值接收者,可以简单的看成一个形参,而修改一个形参的值,并不会对方法外的值造成任何影响,
那么如果通过指针调用会如何呢?
func main() {
myInt := MyInt(1)
(&myInt).Set(2)
fmt.Println(myInt)
}
遗憾的是,这样的代码依旧不能修改内部的值,为了能够匹配上接收者的类型,Go会将其解引用,解释为(*(&myInt)).Set(2)。
c.指针接收者
稍微修改了一下,就能正常修改myInt的值。
type MyInt int
func (i *MyInt) Set(val int) {
*i = MyInt(val)
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}
现在的接收者就是一个指针接收者,虽然myInt是一个值类型,在通过值类型调用指针接收者的方法时,
Go会将其解释为(&myint).Set(2)。所以方法的接收者为指针时,不管调用者是不是指针,都可以修改内部的值。
---------------------------------------------------------------------------------------------------------
函数的参数传递过程中,是值拷贝的,如果传递的是一个整型,那就拷贝这个整型,如果是一个切片,那就拷贝这个切片,
但如果是一个指针,就只需要拷贝这个指针,显然传递一个指针比起传递一个切片所消耗的资源更小,接收者也不例外,
值接收者和指针接收者也是同样的道理。在大多数情况下,都推荐使用指针接收者,不过两者并不应该混合使用,
要么都用,要么就都不用,看下面一个例子。
type Animal interface {
Run()
}
type Dog struct {
}
func (d *Dog) Run() {
fmt.Println("Run")
}
func main() {
var an Animal
an = Dog{}
// an = &Dog{} 正确方式
an.Run()
}
---------------------------------------------------------------------------------------------------------
这一段代码将会无法通过编译,编译器将会输出如下错误
cannot use Dog{} (value of type Dog) as type Animal in assignment:
Dog does not implement Animal (Run method has pointer receiver)
---------------------------------------------------------------------------------------------------------
翻译过来就是,无法使用Dog{}初始化Animal类型的变量,因为Dog没有实现Animal ,
解决办法有两种,一是将指针接收者改为值接收者,二是将Dog{}改为&Dog{},接下来逐个讲解。
type Dog struct {
}
func (d Dog) Run() { // 改为了值接收者
fmt.Println("Run")
}
func main() { // 可以正常运行
var an Animal
an = Dog{}
// an = &Dog{} 同样可以
an.Run()
}
在原来的代码中,Run 方法的接收者是*Dog ,自然而然实现Animal接口的就是Dog指针,而不是Dog结构体,这是两个不同的类型,
所以编译器就会认为Dog{}并不是Animal的实现,因此无法赋值给变量an,所以第二种解决办法就是赋值Dog指针给变量an。
不过在使用值接收者时,Dog指针依然可以正常赋值给animal,这是因为Go会在适当情况下对指针进行解引用,
因为通过指针可以找到Dog结构体,但是反过来的话,无法通过Dog结构体找到Dog指针。
如果单纯的在结构体中混用值接收者和指针接收者的话无伤大雅,但是和接口一起使用后,就会出现错误,
倒不如无论何时要么都用值接收者,要么就都用指针接收者,形成一个良好的规范,也可以减少后续维护的负担。
---------------------------------------------------------------------------------------------------------
还有一种情况,就是当值接收者是可寻址的时候,Go会自动的插入指针运算符来进行调用,例如切片是可寻址,依旧可以通过值接收者来修改其内部值。比如下面这个代码
type Slice []int
func (s Slice) Set(i int, v int) {
s[i] = v
}
func main() {
s := make(Slice, 1)
s.Set(0, 1)
fmt.Println(s)
}
输出
[1]
---------------------------------------------------------------------------------------------------------
但这样会引发另一个问题,如果对其添加元素的话,情况就不同了。看下面的例子
type Slice []int
func (s Slice) Set(i int, v int) {
s[i] = v
}
func (s Slice) Append(a int) {
s = append(s, a)
}
func main() {
s := make(Slice, 1, 2)
s.Set(0, 1)
s.Append(2)
fmt.Println(s)
}
[1]
---------------------------------------------------------------------------------------------------------
它的输出还是和之前一样,append函数是有返回值的,向切片添加完元素后必须覆盖原切片,尤其是在扩容后,在方法中对值接收者修改并不会产生任何影响,这也就导致了例子中的结果,改成指针接收者就正常了。
type Slice []int
func (s *Slice) Set(i int, v int) {
(*s)[i] = v
}
func (s *Slice) Append(a int) {
*s = append(*s, a)
}
func main() {
s := make(Slice, 1, 2)
s.Set(0, 1)
s.Append(2)
fmt.Println(s)
}
输出
[1 2]
02.常用信息2
a.方法的定义
接收者类型:方法的接收者可以是值类型或指针类型。接收者类型必须是当前包内定义的命名类型(例如结构体)。
接收者名称:接收者参数可以任意命名,通常使用简短的名称(如 t、u、p 等),也可以省略参数名。
方法签名:方法的定义与函数类似,可以包含参数和返回值。
func (receiver ReceiverType) MethodName(parameters) (returns) {
// method body
}
b.方法的调用
值类型调用:如果方法的接收者是值类型,可以通过值类型实例或指针类型实例来调用。
指针类型调用:如果方法的接收者是指针类型,必须使用指针类型实例来调用,但值类型实例也会被自动转换为指针类型来调用。
v := ValueType{}
v.MethodName() // 值类型调用
p := &ValueType{}
p.MethodName() // 指针类型调用
c.方法集
值接收者的方法集:包括所有接收者为值类型的方法。
指针接收者的方法集:包括接收者为指针类型的方法。指针接收者的方法集包含值接收者的方法集。
d.接收者类型
值接收者:方法操作的是接收者的副本,不会修改原始对象。适用于方法不需要修改对象状态。
指针接收者:方法操作的是接收者的原始对象的指针,可以修改对象的状态。适用于方法需要修改对象状态或避免复制大对象。
e.方法与函数的区别
方法:与特定类型关联,必须指定接收者。可以使用类型实例或指针调用。
函数:独立于任何类型,可以直接调用,不需要接收者。
f.方法重载
不支持重载:Go 不支持方法重载,方法名在同一类型中必须唯一。
g.嵌入
结构体嵌入:通过嵌入(embedding),一个结构体可以包含另一个结构体的方法。嵌入结构体的方法可以直接被调用。
type A struct{}
func (a A) MethodA() {}
type B struct {
A // 嵌入结构体
}
h.方法值与方法表达式
方法值:可以将方法绑定到实例,并作为函数传递。
var f = u.Notify
f() // 调用绑定到实例 u 的 Notify 方法
---------------------------------------------------------------------------------------------------------
方法表达式:创建方法的引用,不需要具体实例。
var f = (*User).Notify
i.自定义方法接口
接口实现:一个类型实现了接口中的方法,则它符合该接口类型的要求。可以通过接口定义方法签名,并实现这些方法。
type Notifier interface {
Notify()
}
type User struct{}
func (u User) Notify() {}
4.2 方法定义
01.方法定义
a.介绍
Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。
• 只能为当前包内命名类型定义方法。
• 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
• 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
• 不支持方法重载,receiver 只是参数签名的组成部分。
• 可用实例 value 或 pointer 调用全部方法,编译器自动转换。
一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
所有给定类型的方法属于该类型的方法集。
b.方法定义:
func (recevier type) methodName(参数列表)(返回值列表){}
参数和返回值可以省略
-------------------------------------------------------------------------------------------------
package main
type Test struct{}
// 无参数、无返回值
func (t Test) method0() {
}
// 单参数、无返回值
func (t Test) method1(i int) {
}
// 多参数、无返回值
func (t Test) method2(x, y int) {
}
// 无参数、单返回值
func (t Test) method3() (i int) {
return
}
// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
return
}
// 无参数、无返回值
func (t *Test) method5() {
}
// 单参数、无返回值
func (t *Test) method6(i int) {
}
// 多参数、无返回值
func (t *Test) method7(x, y int) {
}
// 无参数、单返回值
func (t *Test) method8() (i int) {
return
}
// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
return
}
func main() {}
-----------------------------------------------------------------------------------------------------
下面定义一个结构体类型和该类型的一个方法:
package main
import (
"fmt"
)
//结构体
type User struct {
Name string
Email string
}
//方法
func (u User) Notify() {
fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
// 值类型调用方法
u1 := User{"golang", "golang@golang.com"}
u1.Notify()
// 指针类型调用方法
u2 := User{"go", "go@go.com"}
u3 := &u2
u3.Notify()
}
输出结果:
golang : golang@golang.com
go : go@go.com
-----------------------------------------------------------------------------------------------------
解释:首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,
该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针。
-----------------------------------------------------------------------------------------------------
在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。
注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本
(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作。
-----------------------------------------------------------------------------------------------------
我们修改 Notify 方法,让它的接受者使用指针类型:
package main
import (
"fmt"
)
//结构体
type User struct {
Name string
Email string
}
//方法
func (u *User) Notify() {
fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
// 值类型调用方法
u1 := User{"golang", "golang@golang.com"}
u1.Notify()
// 指针类型调用方法
u2 := User{"go", "go@go.com"}
u3 := &u2
u3.Notify()
}
输出结果:
golang : golang@golang.com
go : go@go.com
-----------------------------------------------------------------------------------------------------
注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。
方法不过是一种特殊的函数,只需将其还原,就知道 receiver T 和 *T 的差别。
package main
import "fmt"
type Data struct {
x int
}
func (self Data) ValueTest() { // func ValueTest(self Data);
fmt.Printf("Value: %p\n", &self)
}
func (self *Data) PointerTest() { // func PointerTest(self *Data);
fmt.Printf("Pointer: %p\n", self)
}
func main() {
d := Data{}
p := &d
fmt.Printf("Data: %p\n", p)
d.ValueTest() // ValueTest(d)
d.PointerTest() // PointerTest(&d)
p.ValueTest() // ValueTest(*p)
p.PointerTest() // PointerTest(p)
}
输出:
Data: 0xc42007c008
Value: 0xc42007c018
Pointer: 0xc42007c008
Value: 0xc42007c020
Pointer: 0xc42007c008
02.普通函数与方法的区别
1.对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
2.对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。
---------------------------------------------------------------------------------------------------------
package main
//普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)
import (
"fmt"
)
//1.普通函数
//接收值类型参数的函数
func valueIntTest(a int) int {
return a + 10
}
//接收指针类型参数的函数
func pointerIntTest(a *int) int {
return *a + 10
}
func structTestValue() {
a := 2
fmt.Println("valueIntTest:", valueIntTest(a))
//函数的参数为值类型,则不能直接将指针作为参数传递
//fmt.Println("valueIntTest:", valueIntTest(&a))
//compile error: cannot use &a (type *int) as type int in function argument
b := 5
fmt.Println("pointerIntTest:", pointerIntTest(&b))
//同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
//fmt.Println("pointerIntTest:", pointerIntTest(&b))
//compile error:cannot use b (type int) as type *int in function argument
}
//2.方法
type PersonD struct {
id int
name string
}
//接收者为值类型
func (p PersonD) valueShowName() {
fmt.Println(p.name)
}
//接收者为指针类型
func (p *PersonD) pointShowName() {
fmt.Println(p.name)
}
func structTestFunc() {
//值类型调用方法
personValue := PersonD{101, "hello world"}
personValue.valueShowName()
personValue.pointShowName()
//指针类型调用方法
personPointer := &PersonD{102, "hello golang"}
personPointer.valueShowName()
personPointer.pointShowName()
//与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
}
func main() {
structTestValue()
structTestFunc()
}
输出结果:
valueIntTest: 12
pointerIntTest: 15
hello world
hello world
hello golang
hello golang
4.3 匿名字段
01.Golang匿名字段 :可以像字段成员那样访问匿名字段方法,编译器负责查找。
package main
import "fmt"
type User struct {
id int
name string
}
type Manager struct {
User
}
func (self *User) ToString() string { // receiver = &(Manager.User)
return fmt.Sprintf("User: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}}
fmt.Printf("Manager: %p\n", &m)
fmt.Println(m.ToString())
}
输出结果:
Manager: 0xc42000a060
User: 0xc42000a060, &{1 Tom}
02.通过匿名字段,可获得和继承类似的复用能力。依据编译器查找次序,只需在外层定义同名方法,就可以实现 "override"。
package main
import "fmt"
type User struct {
id int
name string
}
type Manager struct {
User
title string
}
func (self *User) ToString() string {
return fmt.Sprintf("User: %p, %v", self, self)
}
func (self *Manager) ToString() string {
return fmt.Sprintf("Manager: %p, %v", self, self)
}
func main() {
m := Manager{User{1, "Tom"}, "Administrator"}
fmt.Println(m.ToString())
fmt.Println(m.User.ToString())
}
输出结果:
Manager: 0xc420074180, &{{1 Tom} Administrator}
User: 0xc420074180, &{1 Tom}
4.4 方法集
01.方法集
Golang方法集 :每个类型都有与之关联的方法集,这会影响到接口实现规则。
• 类型 T 方法集包含全部 receiver T 方法。
• 类型 *T 方法集包含全部 receiver T + *T 方法。
• 如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
• 如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
• 不管嵌入 T 或 *T,*S 方法集总是包含 T + *T 方法。
用实例 value 和 pointer 调用方法 (含匿名字段) 不受方法集约束,编译器总是查找全部方法,并自动转换 receiver 实参。
---------------------------------------------------------------------------------------------------------
Go 语言中内部类型方法集提升的规则:
类型 T 方法集包含全部 receiver T 方法。
---------------------------------------------------------------------------------------------------------
package main
import (
"fmt"
)
type T struct {
int
}
func (t T) test() {
fmt.Println("类型 T 方法集包含全部 receiver T 方法。")
}
func main() {
t1 := T{1}
fmt.Printf("t1 is : %v\n", t1)
t1.test()
}
输出结果:
t1 is : {1}
---------------------------------------------------------------------------------------------------------
类型 T 方法集包含全部 receiver T 方法。
类型 *T 方法集包含全部 receiver T + *T 方法。
package main
import (
"fmt"
)
type T struct {
int
}
func (t T) testT() {
fmt.Println("类型 *T 方法集包含全部 receiver T 方法。")
}
func (t *T) testP() {
fmt.Println("类型 *T 方法集包含全部 receiver *T 方法。")
}
func main() {
t1 := T{1}
t2 := &t1
fmt.Printf("t2 is : %v\n", t2)
t2.testT()
t2.testP()
}
输出结果:
t2 is : &{1}
类型 *T 方法集包含全部 receiver T 方法。
类型 *T 方法集包含全部 receiver *T 方法。
---------------------------------------------------------------------------------------------------------
给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
这条规则说的是当我们嵌入一个类型,嵌入类型的接受者为值类型的方法将被提升,可以被外部类型的值和指针调用。
package main
import (
"fmt"
)
type S struct {
T
}
type T struct {
int
}
func (t T) testT() {
fmt.Println("如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。")
}
func main() {
s1 := S{T{1}}
s2 := &s1
fmt.Printf("s1 is : %v\n", s1)
s1.testT()
fmt.Printf("s2 is : %v\n", s2)
s2.testT()
}
输出结果:
s1 is : {{1}}
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
s2 is : &{{1}}
如类型 S 包含匿名字段 T,则 S 和 *S 方法集包含 T 方法。
---------------------------------------------------------------------------------------------------------
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T + *T 方法。
这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接受者为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
package main
import (
"fmt"
)
type S struct {
T
}
type T struct {
int
}
func (t T) testT() {
fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法")
}
func (t *T) testP() {
fmt.Println("如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法")
}
func main() {
s1 := S{T{1}}
s2 := &s1
fmt.Printf("s1 is : %v\n", s1)
s1.testT()
s1.testP()
fmt.Printf("s2 is : %v\n", s2)
s2.testT()
s2.testP()
}
输出结果:
s1 is : {{1}}
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法
s2 is : &{{1}}
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 T 方法
如类型 S 包含匿名字段 *T,则 S 和 *S 方法集包含 *T 方法
4.5 表达式
01.表达式
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}
---------------------------------------------------------------------------------------------------------
在汇编层面,method value 和闭包的实现方式相同,实际返回 FuncVal 类型对象。
FuncVal { method_address, receiver_copy }
---------------------------------------------------------------------------------------------------------
可依据方法集转换 method expression,注意 receiver 类型的差异。
package main
import "fmt"
type User struct {
id int
name string
}
func (self *User) TestPointer() {
fmt.Printf("TestPointer: %p, %v\n", self, self)
}
func (self User) TestValue() {
fmt.Printf("TestValue: %p, %v\n", &self, self)
}
func main() {
u := User{1, "Tom"}
fmt.Printf("User: %p, %v\n", &u, u)
mv := User.TestValue
mv(u)
mp := (*User).TestPointer
mp(&u)
mp2 := (*User).TestValue // *User 方法集包含 TestValue。签名变为 func TestValue(self *User)。实际依然是 receiver value copy。
mp2(&u)
}
输出:
User: 0xc42000a060, {1 Tom}
TestValue: 0xc42000a0a0, {1 Tom}
TestPointer: 0xc42000a060, &{1 Tom}
TestValue: 0xc42000a100, {1 Tom}
---------------------------------------------------------------------------------------------------------
将方法 "还原" 成函数,就容易理解下面的代码了。
package main
type Data struct{}
func (Data) TestValue() {}
func (*Data) TestPointer() {}
func main() {
var p *Data = nil
p.TestPointer()
(*Data)(nil).TestPointer() // method value
(*Data).TestPointer(nil) // method expression
// p.TestValue() // invalid memory address or nil pointer dereference
// (Data)(nil).TestValue() // cannot convert nil to type Data
// Data.TestValue(nil) // cannot use nil as type Data in function argument
}
01.接口
a.接口类型
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。
b.为什么要使用接口
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语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。
c.接口的定义
a.Go语言提倡面向接口编程。
接口是一个或多个方法签名的集合。
任何类型的方法集中只要拥有该接口'对应的全部方法'签名。
就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。
这称为Structural Typing。
所谓对应方法,是指有相同名称、参数列表 (不包括参数名) 以及返回值。
当然,该类型还可以有其他方法。
-------------------------------------------------------------------------------------------------
接口只有方法声明,没有实现,没有数据字段。
接口可以匿名嵌入其他接口,或嵌入到结构中。
对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针。
只有当接口存储的类型和对象都为nil时,接口才等于nil。
接口调用不会做receiver的自动转换。
接口同样支持匿名字段方法。
接口也可实现类似OOP中的多态。
空接口可以作为任何类型数据的容器。
一个类型可实现多个接口。
接口命名习惯以 er 结尾。
b.每个接口由数个方法组成,接口的定义格式如下:
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
其中:
1.接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
2.方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
3.参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
c.举个例子:
type writer interface{
Write([]byte) error
}
当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。
d.实现接口的条件
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
我们来定义一个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("喵喵喵")
}
接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。
e.接口类型变量
那实现了接口有什么用呢?
接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。
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() // 汪汪汪
}
f.值接收者和指针接收者实现接口的区别
使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?接下来我们通过一个例子看一下其中的区别。
我们有一个Mover接口和一个dog结构体。
type Mover interface {
move()
}
type dog struct {}
g.值接收者实现接口
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。
h.指针接收者实现接口
同样的代码我们再来测试一下使用指针接收者有什么区别:
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类型的值。
i.下面的代码是一个比较好的面试题
请问下面的代码是否能通过编译?
type People interface {
Speak(string) string
}
type Student struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "sb" {
talk = "你是个大帅比"
} else {
talk = "您好"
}
return
}
func main() {
var peo People = Student{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
02.类型与接口的关系
a.一个类型实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
例如,狗可以叫,也可以动。我们就分别定义Sayer接口和Mover接口,如下: Mover接口。
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
dog既可以实现Sayer接口,也可以实现Mover接口。
type dog struct {
name string
}
// 实现Sayer接口
func (d dog) say() {
fmt.Printf("%s会叫汪汪汪\n", d.name)
}
// 实现Mover接口
func (d dog) move() {
fmt.Printf("%s会动\n", d.name)
}
func main() {
var x Sayer
var y Mover
var a = dog{name: "旺财"}
x = a
y = a
x.say()
y.move()
}
b.多个类型实现同一接口
Go语言中不同的类型还可以实现同一接口 首先我们定义一个Mover接口,它要求必须由一个move方法。
// Mover 接口
type Mover interface {
move()
}
例如狗可以动,汽车也可以动,可以使用如下代码实现这个关系:
type dog struct {
name string
}
type car struct {
brand string
}
// dog类型实现Mover接口
func (d dog) move() {
fmt.Printf("%s会跑\n", d.name)
}
// car类型实现Mover接口
func (c car) move() {
fmt.Printf("%s速度70迈\n", c.brand)
}
这个时候我们在代码中就可以把狗和汽车当成一个会动的物体来处理了,不再需要关注它们具体是什么,只需要调用它们的move方法就可以了。
func main() {
var x Mover
var a = dog{name: "旺财"}
var b = car{brand: "保时捷"}
x = a
x.move()
x = b
x.move()
}
上面的代码执行结果如下:
旺财会跑
保时捷速度70迈
并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// WashingMachine 洗衣机
type WashingMachine interface {
wash()
dry()
}
// 甩干器
type dryer struct{}
// 实现WashingMachine接口的dry()方法
func (d dryer) dry() {
fmt.Println("甩一甩")
}
// 海尔洗衣机
type haier struct {
dryer //嵌入甩干器
}
// 实现WashingMachine接口的wash()方法
func (h haier) wash() {
fmt.Println("洗刷刷")
}
c.接口嵌套
接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:
type cat struct {
name string
}
func (c cat) say() {
fmt.Println("喵喵喵")
}
func (c cat) move() {
fmt.Println("猫会动")
}
func main() {
var x animal
x = cat{name: "花花"}
x.move()
x.say()
}
03.空接口
a.空接口的定义
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量。
func main() {
// 定义一个空接口x
var x interface{}
s := "pprof.cn"
x = s
fmt.Printf("type:%T value:%v\n", x, x)
i := 100
x = i
fmt.Printf("type:%T value:%v\n", x, x)
b := true
x = b
fmt.Printf("type:%T value:%v\n", x, x)
}
b.空接口的应用
空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
fmt.Printf("type:%T value:%v\n", a, a)
}
空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
c.类型断言
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
接口值
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
我们来看一个具体的例子:
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
举个例子:
func main() {
var x interface{}
x = "pprof.cn"
v, ok := x.(string)
if ok {
fmt.Println(v)
} else {
fmt.Println("类型断言失败")
}
}
上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
5.4 类型1
01.静态强类型
Go是一个静态强类型语言,静态指的是Go所有变量的类型早在编译期间就已经确定了,在程序的生命周期都不会再发生改变,
尽管Go中的短变量声明有点类似动态语言的写法,但其变量类型是由编译器自行推断的,最根本的区别在于它的类型一旦推断出来
后不会再发生变化,动态语言则完全相反。所以下面的代码完全无法通过编译,因为a是int类型的变量,不能赋值字符串。
func main() {
var a int = 64
a = "64"
fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}
---------------------------------------------------------------------------------------------------------
强类型则指的是在程序中执行严格的类型检查,如果出现类型不匹配的情况时,会立即告诉程序员不应该这么做,
而不是像动态语言一样去尝试推断可能的结果。所以下面的代码无法通过编译,因为两者类型不同,无法进行运算。
func main() {
fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}
02.类型后置
Go为什么要把类型声明放在后面而不是前面,很大程度上是从C语言吸取了教训,拿官方的一个例子展示效果,这是一个函数指针
int (*(*fp)(int (*)(int, int), int))(int, int)
---------------------------------------------------------------------------------------------------------
说实话不认真看很难知道这是一个什么类型,在Go中类似的写法如下
f func(func(int,int) int, int) func(int, int) int
Go的声明方式始终遵循名字在前面,类型在后面的原则,从左往右读,大概第一眼就可以知道这是一个函数,
且返回值为func(int,int) int。当类型变得越来越复杂时,类型后置在可读性上要好得多,
Go在许多层面的设计都是为了可读性而服务的。
03.类型声明
在Go中通过类型声明,可以声明一个自定义名称的新类型,声明一个新类型通常需要一个类型名称以及一个基础类型,简单的例子如下:
type MyInt int64
---------------------------------------------------------------------------------------------------------
在上述类型声明中,通过type关键字声明了一个基础类型为int64名为MyInt的类型。在Go中,每一个新声明的类型都必须有一个与之对应的基础类型,且类型名称不建议与已有的内置标识符重复。
type MyInt int64
type MyFloat64 float64
type MyMap map[string]int
// 可以通过编译,但是不建议使用,这会覆盖原有的类型
type int int64
---------------------------------------------------------------------------------------------------------
通过类型声明的类型都是新类型,不同的类型无法进行运算,即便基础类型是相同的。
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)
invalid operation: f1 + f (mismatched types MyFloat64 and float64)
04.类型别名
类型别名与类型声明则不同,类型别名仅仅只是一个别名,并不是创建了一个新的类型,简单的例子如下:
type Int = int
---------------------------------------------------------------------------------------------------------
两者是都是同一个类型,仅仅叫的名字不同,所以也就可以进行运算,所以下例自然也就可以通过编译。
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)
3
---------------------------------------------------------------------------------------------------------
类型别名对于一些特别复杂的类型有很大的用处,例如现在有一个类型map[string]map[string]int,这是一个二维map,
现有一个函数参数是map[string]map[string]int类型,如下
func PrintMyMap(mymap map[string]map[string]int) {
fmt.Println(mymap)
}
---------------------------------------------------------------------------------------------------------
这种情况下,就没有必要使用类型声明了,因为前者是声明了一个新的类型,无法作为该函数的参数,使用类型别名后的例子如下
type TwoDMap = map[string]map[string]int
func PrintMyMap(mymap TwoDMap) {
fmt.Println(mymap)
}
使用类型别名后看起来会简洁一些。
---------------------------------------------------------------------------------------------------------
提示:内置类型any就是interface{}的类型别名,两者完全等价,仅仅叫法不一样。
05.类型转换
在Go中,只存在显式的类型转换,不存在隐式类型转换,因此不同类型的变量无法进行运算,无法作为参数传递。类型转换适用的前提是知晓被转换变量的类型和要转换成的目标类型,例子如下:
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)
0.30000000000000004
---------------------------------------------------------------------------------------------------------
通过显式的将MyFloat64 转换为float64类型,才能进行加法运算。类型转换的另一个前提是:被转换类型必须是可以被目标类型
代表的(Representability),例如int可以被int64类型所代表,也可以被float64类型代表,所以它们之间可以进行显式的类型
转换,但是int类型无法被string和bool类型代表,因为也就无法进行类型转换。
---------------------------------------------------------------------------------------------------------
即便两个类型可以相互代表,类型转换的结果也不总是正确的,看下面的一个例子:
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))
1 0
---------------------------------------------------------------------------------------------------------
num1被正确的转换为了int32类型,但是num2并没有。这是一个典型的数值溢出问题,int32能够表示31位整数,
int8只能表示7位整数,高精度整数在向低精度整数转换时会抛弃高位保留低位,因此num1的转换结果就是0。
在数字的类型转换中,通常建议小转大,而不建议大转小。
在使用类型转换时,对于一些类型需要避免歧义,例子如下:
*Point(p) // 等价于 *(Point(p))
(*Point)(p) // 将p转换为类型 *Point
<-chan int(c) // 等价于 <-(chan int(c))
(<-chan int)(c) // 将c转换为类型 <-chan int
(func())(x) // 将x转换为类型 func()
(func() int)(x) // 将x转换为类型 func() int
06.类型断言
类型断言通常用于判断某一接口类型的变量是否属于某一个类型,示例如下
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}
1
---------------------------------------------------------------------------------------------------------
由于interface{}是空接口类型,空接口类型可以代表所有的类型,但是int类型无法代表interface{}类型,
所以无法使用类型转换。而类型断言就可以判断其底层类型是否为想要的类型,类型断言语句有两个返回值,
一个是类型转换过后的值,另一个是转换结果的布尔值。
07.类型判断
在Go中,switch语句还支持一种特殊的写法,通过这种写法可以根据不同的case做出不同的逻辑处理,使用的前提是入参必须是接口类型,示例如下:
var a interface{} = 2
switch a.(type) {
case int: fmt.Println("int")
case float64: fmt.Println("float")
case string: fmt.Println("string")
}
int
5.5 类型2
00.go不支持隐式转换类型
package main
import "fmt"
func main() {
var a int64 = 3
var b int32
b = a
fmt.Printf("b 为 : %d", b)
}
---------------------------------------------------------------------------------------------------------
此时会报错
cannot use a (type int64) as type int32 in assignment
cannot use b (type int32) as type string in argument to fmt.Printf
---------------------------------------------------------------------------------------------------------
但是如果改成 b = int32(a) 就不会报错了:
package main
import "fmt"
func main() {
var a int64 = 3
var b int32
b = int32(a)
fmt.Printf("b 为 : %d", b)
}
01.数据转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。
Go 语言类型转换基本格式如下:
type_name(expression)
type_name 为类型,expression 为表达式。
02.数值类型转换
将整型转换为浮点型:
var a int = 10
var b float64 = float64(a)
---------------------------------------------------------------------------------------------------------
以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:
package main
import "fmt"
func main() {
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean)
}
以上实例执行输出结果为:
mean 的值为: 3.400000
03.字符串类型转换
将一个字符串转换成另一个类型,可以使用以下语法:
var str string = "10"
var num int
num, _ = strconv.Atoi(str)
以上代码将字符串变量 str 转换为整型变量 num。
注意,strconv.Atoi 函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
---------------------------------------------------------------------------------------------------------
a.以下实例将字符串转换为整数
package main
import (
"fmt"
"strconv"
)
func main() {
str := "123"
num, err := strconv.Atoi(str)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)
}
}
以上实例执行输出结果为:
字符串 '123' 转换为整数为:123
b.以下实例将整数转换为字符串:
package main
import (
"fmt"
"strconv"
)
func main() {
num := 123
str := strconv.Itoa(num)
fmt.Printf("整数 %d 转换为字符串为:'%s'\n", num, str)
}
以上实例执行输出结果为:
整数 123 转换为字符串为:'123'
c.以下实例将字符串转换为浮点数:
package main
import (
"fmt"
"strconv"
)
func main() {
str := "3.14"
num, err := strconv.ParseFloat(str, 64)
if err != nil {
fmt.Println("转换错误:", err)
} else {
fmt.Printf("字符串 '%s' 转为浮点型为:%f\n", str, num)
}
}
以上实例执行输出结果为:
字符串 '3.14' 转为浮点型为:3.140000
d.以下实例将浮点数转换为字符串:
package main
import (
"fmt"
"strconv"
)
func main() {
num := 3.14
str := strconv.FormatFloat(num, 'f', 2, 64)
fmt.Printf("浮点数 %f 转为字符串为:'%s'\n", num, str)
}
以上实例执行输出结果为:
浮点数 3.140000 转为字符串为:'3.14'
e.string转int、int转string
package main
import (
"fmt"
"strconv"
)
func main() {
// string to int
aStr := "100"
bInt, err := strconv.Atoi(aStr)
if err == nil {
fmt.Printf("aStr:%T %s,bInt:%T %d", aStr, aStr, bInt, bInt)
} else {
fmt.Printf("err:%s", err)
}
// int to string
cInt := 200
dStr := strconv.Itoa(cInt)
fmt.Printf("cInt:%T %d,dStr:%T %s", cInt, cInt, dStr, dStr)
}
03.接口类型转换有两种情况:类型断言和类型转换。
a.类型断言
类型断言用于将接口类型转换为指定类型,其语法为:
value.(type)
或者
value.(T)
其中 value 是接口类型的变量,type 或 T 是要转换成的类型。
如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功。
-----------------------------------------------------------------------------------------------------
package main
import "fmt"
func main() {
var i interface{} = "Hello, World"
str, ok := i.(string)
if ok {
fmt.Printf("'%s' is a string\n", str)
} else {
fmt.Println("conversion failed")
}
}
以上实例中,我们定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"。然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息。
b.类型转换
类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:
T(value)
T 是目标接口类型,value 是要转换的值。
在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。
-----------------------------------------------------------------------------------------------------
package main
import "fmt"
// 定义一个接口 Writer
type Writer interface {
Write([]byte) (int, error)
}
// 实现 Writer 接口的结构体 StringWriter
type StringWriter struct {
str string
}
// 实现 Write 方法
func (sw *StringWriter) Write(data []byte) (int, error) {
sw.str += string(data)
return len(data), nil
}
func main() {
// 创建一个 StringWriter 实例并赋值给 Writer 接口变量
var w Writer = &StringWriter{}
// 将 Writer 接口类型转换为 StringWriter 类型
sw := w.(*StringWriter)
// 修改 StringWriter 的字段
sw.str = "Hello, World"
// 打印 StringWriter 的字段值
fmt.Println(sw.str)
}
-----------------------------------------------------------------------------------------------------
定义接口和结构体:
Writer 接口定义了 Write 方法。
StringWriter 结构体实现了 Write 方法。
类型转换:
将 StringWriter 实例赋值给 Writer 接口变量 w。
使用 w.(*StringWriter) 将 Writer 接口类型转换为 StringWriter 类型。
访问字段:
修改 StringWriter 的字段 str,并打印其值。
04.空接口类型
空接口 interface{} 可以持有任何类型的值。在实际应用中,空接口经常被用来处理多种类型的值。
package main
import (
"fmt"
)
func printValue(v interface{}) {
switch v := v.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
}
func main() {
printValue(42)
printValue("hello")
printValue(3.14)
}
5.6 泛型
00.背景
最初的Go是没有泛型这一说法的,但自从诞生以来,社区关于Go呼声最高的事情就是希望加入泛型。
终于Go在1.18版本加入了对泛型的支持,不过有一点怪。
01.示例
在开始之前,先来看一个简单的例子。
func Sum(a, b int) int {
return a + b
}
---------------------------------------------------------------------------------------------------------
这是一个功能十分简单的函数,作用就是将两个int类型的整数相加并返回结果,倘若想要传入两个float64类型的浮点数求和的话,显然是不可以的,因为类型不匹配。一种解决办法就是再定义一个新的函数,如下
func SumFloat64(a, b float64) float64 {
return a + b
}
---------------------------------------------------------------------------------------------------------
那么问题来了,如果开发一个数学工具包,计算所有数字类型的两数之和,难道要每一个类型都要编写一个函数吗?显然是不太可能的,或者也可以使用any类型加反射来判断,如下
func SumAny(a, b any) (any, error) {
tA, tB := reflect.ValueOf(a), reflect.ValueOf(b)
if tA.Kind() != tB.Kind() {
return nil, errors.New("disMatch type")
}
switch tA.Kind() {
case reflect.Int:
case reflect.Int32:
...
}
}
---------------------------------------------------------------------------------------------------------
但是这样写会显得十分复杂,而且性能低下。但是Sum函数的逻辑都是一模一样的,都只不过是将两个数相加而已,这时候就需要用到了泛型,所以为什么需要泛型,泛型是为了解决执行逻辑与类型无关的问题,这类问题不关心给出的类型是什么,只需要完成对应的操作就足够。所以泛型的写法如下
func Sum[T int | float64](a, b T) T {
return a + b
}
类型形参:T就是一个类型形参,形参具体是什么类型取决于传进来什么类型
类型约束:int | float64构成了一个类型约束,这个类型约束内规定了哪些类型是允许的,约束了类型形参的类型范围
类型实参:Sum[int](1,2),手动指定了int类型,int就是类型实参。
---------------------------------------------------------------------------------------------------------
第一种用法,显式的指明使用哪种类型,如下
Sum[int](2012, 2022)
第二种用法,不指定类型,让编译器自行推断,如下
Sum(3.1415926, 1.114514)
看到这里后,应该对为什么要使用泛型,以及泛型解决了哪种问题有了一个大概的了解。将泛型引入项目后,开发上确实会比较方便,随之而来的是项目复杂度的增加,毫无节制的使用泛型会使得代码难以维护,所以应该在正确的地方使用泛型,而不是为了泛型而泛型。
02.泛型结构
a.介绍
这是一个泛型切片,类型约束为int | int32 | int64
type GenericSlice[T int | int32 | int64] []T
-----------------------------------------------------------------------------------------------------
这里使用时就不能省略掉类型实参
GenericSlice[int]{1, 2, 3}
-----------------------------------------------------------------------------------------------------
这是一个泛型哈希表,键的类型必须是可比较的,所以使用comparable接口,值的类型约束为V int | string | byte
type GenericMap[K comparable, V int | string | byte] map[K]V
使用
gmap1 := GenericMap[int, string]{1: "hello world"}
gmap2 := make(GenericMap[string, byte], 0)
-----------------------------------------------------------------------------------------------------
这是一个泛型结构体,类型约束为T int | string
type GenericStruct[T int | string] struct {
Name string
Id T
}
使用
GenericStruct[int]{
Name: "jack",
Id: 1024,
}
GenericStruct[string]{
Name: "Mike",
Id: "1024",
}
-----------------------------------------------------------------------------------------------------
这是一个泛型切片形参的例子
type Company[T int | string, S []T] struct {
Name string
Id T
Stuff S
}
//也可以如下
type Company[T int | string, S []int | string] struct {
Name string
Id T
Stuff S
}
使用
Company[int, []int]{
Name: "lili",
Id: 1,
Stuff: []int{1},
}
-----------------------------------------------------------------------------------------------------
提示
在泛型结构体中,更推荐这种写法
type Company[T int | string, S int | string] struct {
Name string
Id T
Stuff []S
}
SayAble是一个泛型接口,Person实现了该接口。
type SayAble[T int | string] interface {
Say() T
}
type Person[T int | string] struct {
msg T
}
func (p Person[T]) Say() T {
return p.msg
}
func main() {
var s SayAble[string]
s = Person[string]{"hello world"}
fmt.Println(s.Say())
}
b.泛型结构注意点
泛型不能作为一个类型的基本类型
以下写法是错误的,泛型形参T是不能作为基础类型的
type GenericType[T int | int32 | int64] T
虽然下列的写法是允许的,不过毫无意义而且可能会造成数值溢出的问题,虽然并不推荐
type GenericType[T int | int32 | int64] int
-----------------------------------------------------------------------------------------------------
泛型类型无法使用类型断言
对泛型类型使用类型断言将会无法通过编译,泛型要解决的问题是类型无关的,如果一个问题需要根据不同类型做出不同的逻辑,那么就根本不应该使用泛型,应该使用interface{}或者any。
func Sum[T int | float64](a, b T) T {
ints,ok := a.(int) // 不被允许
switch a.(type) { // 不被允许
case int:
case bool:
...
}
return a + b
}
-----------------------------------------------------------------------------------------------------
匿名结构不支持泛型
匿名结构体是不支持泛型的,如下的代码将无法通过编译
testStruct := struct[T int | string] {
Name string
Id T
}[int]{
Name: "jack",
Id: 1
}
-----------------------------------------------------------------------------------------------------
匿名函数不支持自定义泛型
以下两种写法都将无法通过编译
var sum[T int | string] func (a, b T) T
sum := func[T int | string](a,b T) T{
...
}
但是可以使用已有的泛型类型,例如闭包中
func Sum[T int | float64](a, b T) T {
sub := func(c, d T) T {
return c - d
}
return sub(a,b) + a + b
}
-----------------------------------------------------------------------------------------------------
不支持泛型方法
方法是不能拥有泛型形参的,但是receiver可以拥有泛型形参。如下的代码将会无法通过编译
type GenericStruct[T int | string] struct {
Name string
Id T
}
func (g GenericStruct[T]) name[S int | float64](a S) S {
return a
}
03.类型集
a.介绍
a.介绍
在1.18以后,接口的定义变为了类型集(type set),含有类型集的接口又称为General interfaces即通用接口。
An interface type defines a type set
类型集主要用于类型约束,不能用作类型声明,既然是集合,就会有空集,并集,交集,接下来将会讲解这三种情况。
b.并集
接口类型SignedInt是一个类型集,有符号整数类型的并集就是SignedInt,反过来SignedInt就是它们的超集。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
基本数据类型如此,对待其它通用接口也是如此
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnSignedInt interface {
uint8 | uint16 | uint32 | uint64
}
type Integer interface {
SignedInt | UnSignedInt
}
c.交集
非空接口的类型集是其所有元素的类型集的交集,翻译成人话就是:如果一个接口包含多个非空类型集,那么该接口就是这些类型集的交集,例子如下
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type Integer interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type Number interface {
SignedInt
Integer
}
例子中的交集肯定就是SignedInt,
func Do[T Number](n T) T {
return n
}
Do[int](2)
DO[uint](2) //无法通过编译
d.空集
空集就是没有交集,例子如下,下面例子中的Integer就是一个类型空集。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
type UnsignedInt interface {
uint8 | uint16 | uint | uint32 | uint64
}
type Integer interface {
SignedInt
UnsignedInt
}
因为无符号整数和有符号整数两个肯定没有交集,所以交集就是个空集,下方例子中不管传什么类型都无法通过编译。
Do[Integer](1)
Do[Integer](-100)
e.空接口
空接口与空集并不同,空接口是所有类型集的集合,即包含所有类型。
func Do[T interface{}](n T) T {
return n
}
func main() {
Do[struct{}](struct{}{})
Do[any]("abc")
}
f.底层类型
当使用type关键字声明了一个新的类型时,即便其底层类型包含在类型集内,当传入时也依旧会无法通过编译。
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64
}
type TinyInt int8
func Do[T Int](n T) T {
return n
}
func main() {
Do[TinyInt](1) // 无法通过编译,即便其底层类型属于Int类型集的范围内
}
-------------------------------------------------------------------------------------------------
有两种解决办法,第一种是往类型集中并入该类型,但是这毫无意义,因为TinyInt与int8底层类型就是一致的,所以就有了第二种解决办法。
type Int interface {
int8 | int16 | int | int32 | int64 | uint8 | uint16 | uint | uint32 | uint64 | TinyInt
}
使用~符号,来表示底层类型,如果一个类型的底层类型属于该类型集,那么该类型就属于该类型集,如下所示
-------------------------------------------------------------------------------------------------
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64
}
修改过后就可以通过编译了。
func main() {
Do[TinyInt](1) // 可以通过编译,因为TinyInt在类型集Int内
}
b.类型集注意点
a.带有方法集的接口无法并入类型集
只要是带有方法集的接口,不论是基本接口,泛型接口,又或者是通用接口,都无法并入类型集中,同样的也无法在类型约束中并入。以下两种写法都是错误的,都无法通过编译。
type Integer interface {
Sum(int, int) int
Sub(int, int) int
}
type SignedInt interface {
int8 | int16 | int | int32 | int64 | Integer
}
func Do[T Integer | float64](n T) T {
return n
}
b.类型集无法当作类型实参使用
只要是带有类型集的接口,都无法当作类型实参。
type SignedInt interface {
int8 | int16 | int | int32 | int64
}
func Do[T SignedInt](n T) T {
return n
}
func main() {
Do[SignedInt](1) // 无法通过编译
}
c.类型集中的交集问题
对于非接口类型,类型并集中不能有交集,例如下例中的TinyInt与~int8有交集。
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 无法通过编译
}
type TinyInt int8
但是对于接口类型的话,就允许有交集,如下例
type Int interface {
~int8 | ~int16 | ~int | ~int32 | ~int64 | ~uint8 | ~uint16 | ~uint | ~uint32 | ~uint64 | TinyInt // 可以通过编译
}
type TinyInt interface {
int8
}
d.类型集不能直接或间接的并入自身
以下示例中,Floats 直接的并入了自身,而Double又并入了Floats,所以又间接的并入了自身。
type Floats interface { // 代码无法通过编译
Floats | Double
}
type Double interface {
Floats
}
e.comparable接口无法并入类型集,同样的,也无法并入类型约束中,所以基本上都是单独使用。
func Do[T comparable | Integer](n T) T { //无法通过编译
return n
}
type Number interface { // 无法通过编译
Integer | comparable
}
type Comparable interface { // 可以通过编译但是毫无意义
comparable
}
04.使用
a.队列
下面用泛型实现一个简单的队列,首先声明队列类型,队列中的元素类型可以是任意的,所以类型约束为any
type Queue[T any] []T
-----------------------------------------------------------------------------------------------------
总共只有四个方法Pop ,Peek,Push,Size,代码如下。
type Queue[T any] []T
func (q *Queue[T]) Push(e T) {
*q = append(*q, e)
}
func (q *Queue[T]) Pop(e T) (_ T) {
if q.Size() > 0 {
res := q.Peek()
*q = (*q)[1:]
return res
}
return
}
func (q *Queue[T]) Peek() (_ T) {
if q.Size() > 0 {
return (*q)[0]
}
return
}
func (q *Queue[T]) Size() int {
return len(*q)
}
-----------------------------------------------------------------------------------------------------
在Pop和Peek方法中,可以看到返回值是_ T,这是具名返回值的使用方式,但是又采用了下划线_表示这是匿名的,
这并非多此一举,而是为了表示泛型零值。由于采用了泛型,当队列为空时,需要返回零值,但由于类型未知,
不可能返回具体的类型,借由上面的那种方式就可以返回泛型零值。也可以声明泛型变量的方式来解决零值问题,
对于一个泛型变量,其默认的值就是该类型的零值,如下
func (q *Queue[T]) Pop(e T) T {
var res T
if q.Size() > 0 {
res = q.Peek()
*q = (*q)[1:]
return res
}
return res
}
b.堆
上面队列的例子,由于对元素没有任何的要求,所以类型约束为any。但堆就不一样了,堆是一种特殊的数据结构,
它可以在O(1)的时间内判断最大或最小值,所以它对元素有一个要求,那就是必须是可以排序的类型,
但内置的可排序类型只有数字和字符串,并且go的泛型约束不允许存在带方法的接口,所以在堆的初始化时,
需要传入一个自定义的比较器,比较器由使用者提供,比较器也必须使用泛型,如下
type Comparator[T any] func(a, b T) int
-----------------------------------------------------------------------------------------------------
下面是一个简单的二项最小堆的实现,先声明泛型结构体,依旧采用any进行约束,这样可以存放任意类型
type Comparator[T any] func(a, b T) int
type BinaryHeap[T any] struct {
s []T
c Comparator[T]
}
-----------------------------------------------------------------------------------------------------
几个方法实现
func (heap *BinaryHeap[T]) Peek() (_ T) {
if heap.Size() > 0 {
return heap.s[0]
}
return
}
func (heap *BinaryHeap[T]) Pop() (_ T) {
size := heap.Size()
if size > 0 {
res := heap.s[0]
heap.s[0], heap.s[size-1] = heap.s[size-1], heap.s[0]
heap.s = heap.s[:size-1]
heap.down(0)
return res
}
return
}
func (heap *BinaryHeap[T]) Push(e T) {
heap.s = append(heap.s, e)
heap.up(heap.Size() - 1)
}
func (heap *BinaryHeap[T]) up(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
for parentIndex := i>>1 - 1; parentIndex >= 0; parentIndex = i>>1 - 1 {
// greater than or equal to
if heap.compare(heap.s[i], heap.s[parentIndex]) >= 0 {
break
}
heap.s[i], heap.s[parentIndex] = heap.s[parentIndex], heap.s[i]
i = parentIndex
}
}
func (heap *BinaryHeap[T]) down(i int) {
if heap.Size() == 0 || i < 0 || i >= heap.Size() {
return
}
size := heap.Size()
for lsonIndex := i<<1 + 1; lsonIndex < size; lsonIndex = i<<1 + 1 {
rsonIndex := lsonIndex + 1
if rsonIndex < size && heap.compare(heap.s[rsonIndex], heap.s[lsonIndex]) < 0 {
lsonIndex = rsonIndex
}
// less than or equal to
if heap.compare(heap.s[i], heap.s[lsonIndex]) <= 0 {
break
}
heap.s[i], heap.s[lsonIndex] = heap.s[lsonIndex], heap.s[i]
i = lsonIndex
}
}
func (heap *BinaryHeap[T]) Size() int {
return len(heap.s)
}
-----------------------------------------------------------------------------------------------------
使用起来如下
type Person struct {
Age int
Name string
}
func main() {
heap := NewHeap[Person](10, func(a, b Person) int {
return cmp.Compare(a.Age, b.Age)
})
heap.Push(Person{Age: 10, Name: "John"})
heap.Push(Person{Age: 18, Name: "mike"})
heap.Push(Person{Age: 9, Name: "lili"})
heap.Push(Person{Age: 32, Name: "miki"})
fmt.Println(heap.Peek())
fmt.Println(heap.Pop())
fmt.Println(heap.Peek())
}
输出
{9 lili}
{9 lili}
{10 John}
-----------------------------------------------------------------------------------------------------
有泛型的加持,原本不可排序的类型传入比较器后也可以使用堆了,这样做肯定比以前使用interface{}来进行类型转换和断言要优雅和方便很多。
05.小结
go的一大特点就是编译速度非常快,编译快是因为编译期做的优化少,泛型的加入会导致编译器的工作量增加,工作更加复杂,
这必然会导致编译速度变慢,事实上当初go1.18刚推出泛型的时候确实导致编译更慢了,go团队既想加入泛型又不想太拖累编译速度,
开发者用的顺手,编译器就难受,反过来编译器轻松了(最轻松的当然是直接不要泛型),开发者就难受了,
现如今的泛型就是这两者之间妥协后的产物。