1.指针
指针(pointer)在Go语言中可以被拆分为两个核心概念:
- 类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
在内存中开辟了一片空间,这片空间在整个内存当中,有一个唯一的地址,用来进行标识,指向这个地址的变量就称为指针。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作)。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。
new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。
2.数组
因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
var 数组变量名 [元素数量]Type
// 给索引为2的赋值 ,所以结果是 0,0,3
arr := [3]int{2:3}
//array_name 为数组的名字,array_type 为数组的类型,size1、size2 等等为数组每一维度的长度。
var array_name [size1][size2]...[sizen] array_type
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
3.切片
将数组引用和动态数组结合起来了。
切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。
与数组不同的是,无法通过切片类型来确定其值的长度。
每个切片值都会将数组作为其底层数据结构。
我们也把这样的数组称为切片的底层数组。
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型。
这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内(左闭右开的区间)。
Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合。
//name 表示切片的变量名,Type 表示切片对应的元素类型。
var name []Type
// 动态创建切片,Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,
// cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
make( []Type, size, cap )
使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。
4.map
//[keytype] 和 valuetype 之间允许有空格。
var mapname map[keytype]valuetype
// 初始化
var mapLit map[string]int
mapLit = map[string]int{"one": 1, "two": 2}
map2 := make(map[string]int, 100)
map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity。
既然一个 key 只能对应一个 value,而 value 又是一个原始类型,那么如果一个 key 要对应多个值怎么办?
答案是:使用切片
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
make 关键字的主要作用是创建 slice、map 和 Channel 等内置的数据结构,而 new 的主要作用是为类型申请一片内存空间,并返回指向这片内存的指针。
- make 分配空间后,会进行初始化,new分配的空间被清零
- new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
- new 可以分配任意类型的数据;
5.函数
5.1 基本概念
Go 语言的函数属于“一等公民”(first-class),也就是说:
- 函数本身可以作为值进行传递。
- 支持匿名函数和闭包(closure)。
- 函数可以满足接口。
func test(fn func() int) int {
return fn()
}
func fn() int {
return 200
}
func main() {
//这是直接使用匿名函数
s1 := test(func() int { return 100 })
fmt.Println(s1)
//这是传入一个函数
s2 := test(fn)
fmt.Println(s2)
}
输出:
100
200
// 定义函数类型。
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 formatFun(s string,x,y int) string {
return fmt.Sprintf(s,x,y)
}
func main() {
s2 := format(formatFun,"%d, %d",10,20)
fmt.Println(s2)
}
结果:10, 20
支持返回值 命名 ,默认值为类型零值,命名返回参数可看做与形参类似的局部变量,由return隐式返回
func f1() (names []string, m map[string]int, num int) {
m = make(map[string]int)
m["k1"] = 2
return
}
func main() {
a, b, c := f1()
fmt.Println(a, b, c)
}
调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数:
- 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
- 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
/* 定义相互交换值的函数 */
func swap(x, y *int) {
*x, *y = *y, *x
}
func main() {
var a, b int = 1, 2
/*
调用 swap() 函数
&a 指向 a 指针,a 变量的地址
&b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Println(a, b)
}
输出:
2 1
注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
注意2:map、slice、chan、指针、interface默认以引用的方式传递。
5.2 不定参数传值
- 不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
- Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
- 在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片。
5.3 匿名函数
func main() {
//这里将一个函数当做一个变量一样的操作。
getSqrt := func(a float64) float64 {
return math.Sqrt(a)
}
fmt.Println(getSqrt(4))
}
func(data int) {
fmt.Println("hello", data)
}(100) //(100),表示对匿名函数进行调用,传递参数为 100。
匿名函数用作回调函数
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
返回多个匿名函数
func FGen(x, y int) (func() int, func(int) int) {
//求和的匿名函数
sum := func() int {
return x + y
}
// (x+y) *z 的匿名函数
avg := func(z int) int {
return (x + y) * z
}
return sum, avg
}
func main() {
f1, f2 := FGen(1, 2)
fmt.Println(f1())
fmt.Println(f2(3))
}
5.3 闭包
“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
闭包=函数+引用环境。
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen1() func(string) (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func(name string) (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
func main() {
// 创建一个玩家生成器
generator := playerGen("ms")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp)
generator1 := playerGen1()
name1, hp1 := generator1("ms")
// 打印值
fmt.Println(name1, hp1)
}
输出:
ms 150
ms 150
5.4 defer
defer特性:
- 关键字 defer 用于注册延迟调用。
- 这些调用直到 return 前才被执。因此,可以用来做资源清理。
- 多个defer语句,按先进后出的方式执行。
- defer语句中的变量,在defer声明时就决定了。
defer的用途:
- 关闭文件句柄
- 锁资源释放
- 数据库连接释放
func main() {
var whatever = [5]int{1, 2, 3, 4, 5}
for i, v := range whatever {
fmt.Println(v)
defer fmt.Println(i)
}
fmt.Println()
}
输出:
1
2
3
4
5
4
3
2
1
0
错误的用法:
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
// Now()此时已经copy进去了
defer log.Printf("时间差:%v", time.Since(start))
//不受这3秒睡眠的影响
time.Sleep(3 * time.Second)
log.Printf("函数结束")
}
结果:
2023/07/08 07:58:40 开始时间为:2023-07-08 07:58:40.1888817 +0800 CST m=+0.002120801
2023/07/08 07:58:43 函数结束
2023/07/08 07:58:43 时间差:10.6131ms
注意点:
- Go 语言中所有的函数调用都是传值的
- 调用 defer 关键字会立刻拷贝函数中引用的外部参数 ,包括start 和time.Since中的Now
- defer的函数在压栈的时候也会保存参数的值,并非在执行时取值。
正确做法:
func main() {
start := time.Now()
log.Printf("开始时间为:%v", start)
defer func() {
log.Printf("开始调用defer")
log.Printf("时间差:%v", time.Since(start))
log.Printf("结束调用defer")
}()
time.Sleep(3 * time.Second)
log.Printf("函数结束")
}
输出:
2023/07/08 08:02:41 开始时间为:2023-07-08 08:02:41.708662 +0800 CST m=+0.001715801
2023/07/08 08:02:44 函数结束
2023/07/08 08:02:44 开始调用defer
2023/07/08 08:02:44 时间差:3.0205091s
2023/07/08 08:02:44 结束调用defer
因为拷贝的是函数指针,函数属于引用传递。
func main() {
var whatever = [5]int{1, 2, 3, 4, 5}
for i, _ := range whatever {
//函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
defer func() { fmt.Println(i) }()
}
}
输出:
4
4
4
4
4
解决方法:
func main() {
var whatever = [5]int{1,2,3,4,5}
for i,_ := range whatever {
i := i
defer func() { fmt.Println(i) }()
}
}
输出:
4
3
2
1
0
5.5 异常处理
如何区别使用 panic 和 error 两种方式?
- 惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic:
- 内置函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
recover:
- 内置函数
- 用来捕获panic,从而影响应用的行为
golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 defer 语句,再报告异常信息,最后退出 goroutine。如果在 defer 中使用了 recover() 函数,则会捕获错误信息,使该错误信息终止报告。
注意:
- 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
- recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
- 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
func main() {
test()
println("two")
}
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
println("one")
}
输出:
panic error!
two
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
func test() {
defer func() {
// defer panic 会打印
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
输出:
defer panic
如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码被执。
func test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil {
z = 0
}
}()
panic("test panic")
z = x / y
return
}()
fmt.Printf("x / y = %d\n", z)
}
func main() {
test(2, 1)
}
输出:
x / y = 0
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。标准库 errors.New 和 fmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
var ErrDivByZero = errors.New("division by zero")
func div(x, y int) (int, error) {
if y == 0 {
return 0, ErrDivByZero
}
return x / y, nil
}
func main() {
defer func() {
fmt.Println(recover())
}()
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
输出:
division by zero
Go实现类似 try catch 的异常处理
func Try(fun func(), handler func(interface{})) {
defer func() {
if err := recover(); err != nil {
handler(err)
}
}()
fun()
}
func main() {
Try(func() {
panic("test panic")
}, func(err interface{}) {
fmt.Println(err)
})
}
输出:
test panic
6.结构体
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
如果不赋值 结构体中的变量会使用零值初始化。
type Point struct {
X int
Y int
}
func main() {
//使用.来访问结构体的成员变量,结构体成员变量的赋值方法与普通变量一致。
var p Point
fmt.Printf("%v,x=%d,y=%d\n", p, p.X, p.Y)
p.X = 1
p.Y = 2
fmt.Printf("%v,x=%d,y=%d\n", p, p.X, p.Y)
//可以使用
var p1 = Point{
X: 1,
Y: 2,
}
var p2 = Point{
1,
2,
}
fmt.Printf("%v,x=%d,y=%d\n", p1, p1.X, p1.Y)
fmt.Printf("%v,x=%d,y=%d\n", p2, p2.X, p2.Y)
}
输出:
{0 0},x=0,y=0
{1 2},x=1,y=2
{1 2},x=1,y=2
{1 2},x=1,y=2
创建指针类型的结构体:
- T 为类型,可以是结构体、整型、字符串等。
- ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。
ins := new(T)
取结构体的地址实例化:
- 在Go语言中,对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作
- T 表示结构体类型。
- ins 为结构体的实例,类型为 *T,是指针类型。
ins := &T{}
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string // 指令的注释
}
func newCommand(name string, varRef *int, comment string) *Command {
return &Command{
Name: name,
Var: varRef,
Comment: comment,
}
}
var version = 1
func main() {
cmd := newCommand(
"version",
&version,
"show version",
)
fmt.Println(cmd)
}
输出:
&{version 0x10bd320 show version}
匿名结构体
ins := struct {
// 匿名结构体字段定义
字段1 字段类型1
字段2 字段类型2
…
}{
// 字段值初始化
初始化字段1: 字段1的值,
初始化字段2: 字段2的值,
…
}
// 打印消息类型, 传入匿名结构体
func printMsgType(msg *struct {
id int
data string
}) {
// 使用动词%T打印msg的类型
fmt.Printf("%T\n, msg:%v", msg,msg)
}
func main() {
// 实例化一个匿名结构体
msg := &struct { // 定义部分
id int
data string
}{ // 值初始化部分
1024,
"hello",
}
printMsgType(msg)
}
输出:
*struct { id int; data string },
msg:&{1024 hello}
7.方法与接收器
一个类型加上它的方法等价于面向对象中的一个类
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。
接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误invalid receiver type。
在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中“方法”的概念与其他语言一致,只是Go语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}
type Property struct {
value int // 属性值
}
// 设置属性值
func (p *Property) SetValue(v int) {
// 修改p的成员变量
p.value = v
}
// 取属性值
func (p *Property) Value() int {
return p.value
}
func main() {
// 实例化属性
p := new(Property)
// 设置值
p.SetValue(100)
// 打印值
fmt.Println(p.Value())
}
输出:
100
在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。
匿名字段:
- 结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。
- 匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。
- Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。
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: 0xc000074510, &{{1 Tom} Administrator}
User: 0xc000074510, &{1 Tom}
8.接口
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,接口做的事情就像是定义一个协议(规则)。不关心属性(数据),只关心行为(方法)。
接口(interface)是一种类型。
接口类型是对其类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。
接口是双方约定的一种合作协议。接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
type error interface {
Error() string
}
接口实现:
errorString是接口error的实现,实现了Error函数。
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
接口实现条件:
- 如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。
- T 可以是一个非接口类型,也可以是一个接口类型。
// 定义一个数据写入器
type DataWriter interface {
WriteData(data interface{}) error
}
// 定义文件结构,用于实现DataWriter
type file struct {
}
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println("WriteData:", data)
return nil
}
func main() {
// 实例化file
f := new(file)
// 声明一个DataWriter的接口
var writer DataWriter
// 将接口赋值f,也就是*file类型
writer = f
// 使用DataWriter接口进行数据写入
writer.WriteData("data")
}
输出:
WriteData: data
一个类型可以实现多个接口。
一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
// 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("洗刷刷")
}
接口与接口间可以通过嵌套创造出新的接口。
// Sayer 接口
type Sayer interface {
say()
}
// Mover 接口
type Mover interface {
move()
}
// 接口嵌套
type animal interface {
Sayer
Mover
}
空接口
- 空接口是指没有定义任何方法的接口。
- 因此任何类型都实现了空接口。
- 空接口类型的变量可以存储任意类型的变量。
空接口作为map的值
- 使用空接口实现可以保存任意值的字典
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
9.包
包的引用有四种格
- 标准引用格式:import "fmt",此时可以用fmt.作为前缀来使用 fmt 包中的方法,这是常用的一种方式。
- 自定义别名引用格式:import F "fmt"
- 省略引用格式:import . "fmt",相当于把 fmt 包直接合并到当前程序中,在使用 fmt 包内的方法是可以不用加前缀fmt.,直接引用。
- 匿名引用格式:在引用某个包时,如果只是希望执行包初始化的 init 函数,而不使用包内部的数据时,可以使用匿名引用格式。
10.go mod
go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。
Modules 官方定义为:
Modules 是相关 Go 包的集合,是源代码交换和版本控制的单元。Go语言命令直接支持使用 Modules,包括记录和解析对其他模块的依赖性,Modules 替换旧的基于 GOPATH 的方法,来指定使用哪些源文件。
使用go module之前需要设置环境变量:
- 1)GO111MODULE=on
- 2)GOPROXY=https://goproxy.io,direct
- 3)GOPROXY=https://goproxy.cn,direct(国内的七牛云提供)
go mod 有以下命令:
![](https://img.haomeiwen.com/i4222138/4bc66b68bae86fe3.png)
执行脚本go run main.go,就可以运行项目。
网友评论