六.循环
1. if else
在Go语言中,关键字` if `是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号`{}`括起来的代码块,否则就忽略该代码块继续执行后续的代码。
1
2
3if condition { // 条件为真执行 }
**condition 称之为条件表达式或者布尔表达式,执行结果需返回true或false。{ 必须在条件表达式的尾部**
如果存在第二个分支,则可以在上面代码的基础上添加 `else `关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。
1
2
3
4
5
6
7x := 5 if x <= 0 { fmt.Println("为真进入这里") //go语言格式要求很严,else必须写在}后面 }else{ fmt.Println("为假进入这里") }
如果存在第三个分支,则可以使用下面这种三个独立分支的形式:
1
2
3
4
5
6
7if condition1 { // condition1 满足 执行 } else if condition2 { // condition1 不满足 condition2满足 执行 }else { // condition1和condition2都不满足 执行 }
1.1 特殊写法
if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:
1
2
3
4if a := 10; a >5 { fmt.Println(a) return }
这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。
> 在编程中,变量的作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行,但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助。
2. for
> go语言中的循环语句只支持 for 关键字,这个其他语言是不同的。
1
2
3
4
5sum := 0 //i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环 for i := 0; i < 10; i++ { sum += i }
第二种写法:
1
2
3
4
5
6
7
8sum := 0 for { sum++ if sum > 100 { //break是跳出循环 break } }
上述的代码,如果没有break跳出循环,那么其将无限循环**
第三种写法:
1
2
3
4
5n := 10 for n>0 { n-- fmt.Println(n) }
结束循环的方式:
1. return —不会执行之后的代码
1
2
3
4
5
6
7
8
9step := 2 for step > 0 { step-- fmt.Println(step) //执行一次就结束了 return } //不会执行 fmt.Println("结束之后的语句....")
2. break —会执行之后的
1
2
3
4
5
6
7
8
9step := 2 for step > 0 { step-- fmt.Println(step) //跳出循环,还会继续执行循环外的语句 break } //会执行 fmt.Println("结束之后的语句....")
3. painc —报错方式跳出循环,后面的不会执行
1
2
3
4
5
6
7
8
9step := 2 for step > 0 { step-- fmt.Println(step) //报错了,直接结束 panic("出错了") } //不会执行 fmt.Println("结束之后的语句....")
4. goto —跳到特定位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15func main() { for x := 0; x < 10; x++ { for y := 0; y < 10; y++ { if y == 2 { // 跳转到标签 goto breakHere } } } // 手动返回, 避免执行进入标签 return // 标签 breakHere: fmt.Println("done") }
2.1 案例
输出九九乘法表
1
2
3
4
5
6
7
8
9
10
11
12
13package main import "fmt" func main() { // 遍历, 决定处理第几行 for y := 1; y <= 9; y++ { // 遍历, 决定这一行有多少列 for x := 1; x <= y; x++ { fmt.Printf("%d*%d=%d ", x, y, x*y) } // 手动生成回车 fmt.Println() } }
3. for range
for range 结构是Go语言特有的一种的迭代结构,for range 可以遍历数组、切片、字符串、map 及管道(channel)
1
2
3
4for key, value := range coll { ... } **`value `始终为集合中对应索引的`值拷贝`,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值**
遍历map:
1
2
3
4
5
6
7m := map[string]int{ "hello": 100, "world": 200, } for key, value := range m { fmt.Println(key, value) }
字符串也可以使用for range:
1
2
3
4
5
6str := "smdongxia" //因为一个字符串是 Unicode 编码的字符(或称之为 rune )集合 //char 实际类型是 rune 类型 for pos, char := range str { fmt.Println(pos,char) }
每个 rune 字符和索引在 for range 循环中是一一对应的,它能够自动根据 UTF-8 规则识别 Unicode 编码的字符。
通过 for range 遍历的返回值有一定的规律:
- 数组、切片、字符串返回索引和值。
- map 返回键和值。
- channel只返回管道内的值。
4. switch
switch 语句的语法如下:
1
2
3
4
5
6
7
8switch var1 { case val1: ... case val2: ... default: ... }
`变量 var1` 可以是任何类型,而 val1 和 val2 则可以是`同类型的任意值`。
类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。
/* 定义局部变量 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23var grade string = "B" var score int = 90 switch score { case 90: grade = "A" case 80: grade = "B" case 50,60,70 : grade = "C" default: grade = "D" } //swtich后面如果没有条件表达式,则会对true进行匹配 //swtich后面如果没有条件表达式,则会对true进行匹配 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("你的等级是 %sn", grade )
Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 那么如何做到执行完一个case之后,进入下一个case而不是跳出swtich呢?
答案是:`fallthrough`
1
2
3
4
5
6
7
8var s = "hello" switch { case s == "hello": fmt.Println("hello") fallthrough case s != "world": fmt.Println("world") }
注意事项:
1. 加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行
1
2
3
4
5
6
7
8var s = "hello" switch { case s == "hello": fmt.Println("hello") fallthrough case s == "world": fmt.Println("world") }
5. goto
> goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。
**使用 goto 退出多层循环**
传统写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main import "fmt" func main() { var breakAgain bool // 外循环 for x := 0; x < 10; x++ { // 内循环 for y := 0; y < 10; y++ { // 满足某个条件时, 退出循环 if y == 2 { // 设置退出标记 breakAgain = true // 退出本次循环 break } } // 根据标记, 还需要退出一次循环 if breakAgain { break } } fmt.Println("done") }
使用goto的写法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main import "fmt" func main() { for x := 0; x < 10; x++ { for y := 0; y < 10; y++ { if y == 2 { // 跳转到标签 goto breakHere } } } // 手动返回, 避免执行进入标签 return // 标签 breakHere: fmt.Println("done") }
6. break
> break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加`标签`,表示退出某个标签对应的代码块,`标签`要求必须定义在对应的 `for`、`switch` 和 `select `的代码块上。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main import "fmt" func main() { OuterLoop: for i := 0; i < 2; i++ { for j := 0; j < 5; j++ { switch j { case 2: fmt.Println(i, j) break OuterLoop case 3: fmt.Println(i, j) break OuterLoop } } } } //输出:0 2
7. continue
continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加`标签`时,表示开始`标签对应的循环`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main import "fmt" func main() { OuterLoop: for i := 0; i < 2; i++ { for j := 0; j < 5; j++ { switch j { case 2: fmt.Println(i, j) continue OuterLoop case 3: fmt.Println(i, j) continue OuterLoop } } } } //输出:0 2 1 2
七.函数
1. 函数
函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率。Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。Go 语言的函数属于“一等公民”(first-class),也就是说:
- 函数本身可以作为值进行传递。
- 支持匿名函数和闭包(closure)。
- 函数可以满足接口。
**函数定义:**
1
2
3func function_name( [parameter list] ) [return_types] { 函数体 }
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为`实际参数`。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:`返回类型,函数返回一列值`。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13package main import "fmt" func main() { fmt.Println(max(1, 10)) fmt.Println(max(-1, -2)) } //类型相同的相邻参数,参数类型可合并。 func max(n1, n2 int) int { if n1 > n2 { return n1 } return n2 }
**Go语言是编译型语言,所以函数编写的顺序是无关紧要的,鉴于可读性的需求,最好把 main() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。**
**返回值可以为多个:
1
2
3
4
5func test(x, y int, s string) (int, string) { // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。 n := x + y return n, fmt.Sprintf(s, n) }
1.1 函数做为参数
函数做为一等公民,可以做为参数传递。
1
2
3
4
5
6
7
8
9
10
11
12
13
14func test(fn func() int) int { return fn() } func fn() int{ return 200 } func main() { //这是直接使用匿名函数 s1 := test(func() int { return 100 }) fmt.Println(s1) //100 //这是传入一个函数 s1 = test(fn) fmt.Println(s1) //200 }
**在将函数做为参数的时候,我们可以使用类型定义,将函数定义为类型,这样便于阅读**
1
2
3
4
5
6
7
8
9
10
11
12// 定义函数类型。 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) }
有返回值的函数,必须有明确的终止语句,否则会引发编译错误。
1.2 函数返回值
函数返回值可以有多个,同时Go支持对返回值命名
~~~go
//多个返回值 用括号扩起来
1
2
3
4
5
6
7func sum(a,b int) (int,int) { return a,b } func main(){ a,b := sum(2,3) fmt.Println(a,b) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main import "fmt" //支持返回值 命名 ,默认值为类型零值,命名返回参数可看做与形参类似的局部变量,由return隐式返回 func f1() (names []string, m map[string]int, num int) { m = make(map[string]int) m["k1"] = 2 m["什么东西"]= 12124 return } func main() { a, b, c := f1() fmt.Println(a, b, c) }
1.3 参数
函数定义时指出,函数定义时有参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
但当`调用函数`,传递过来的变量就是函数的`实参`,函数可以通过两种方式来传递参数:
1. 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
1
2
3
4
5func swap(x, y int) int { ... ... }
2. 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18package main import ( "fmt" ) /* 定义相互交换值的函数 */ 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) }
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
> `注意1:`无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
> `注意2:`map、slice、chan、指针、interface默认以引用的方式传递。
2. 匿名函数
> 匿名函数是指不需要定义函数名的一种函数实现方式。
在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。
匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必声明。
匿名函数的定义格式如下:
func(参数列表)(返回参数列表){
函数体
}
示例:
1
2
3
4
5
6
7
8
9
10
11
12package main import ( "fmt" "math" ) func main() { //这里将一个函数当做一个变量一样的操作。 getSqrt := func(a float64) float64 { return math.Sqrt(a) } fmt.Println(getSqrt(4)) }
在定义时调用匿名函数**
匿名函数可以在声明后调用,例如:
1
2
3
4
5func(data int) { fmt.Println("hello", data) }(100) //(100),表示对匿名函数进行调用,传递参数为 100。
1
2
3
4
5
6
7
8
9
10
11getsqrt := func (shuzi float64) float64 { aaa := math.Sqrt(float64(shuzi)) fmt.Println(aaa) return float64(shuzi) }(12415) fmt.Println(getsqrt)
3. 闭包
> 所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
> 闭包=函数+引用环境
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 创建一个玩家生成器, 输入名称, 输出生成器 func playerGen1() func(string) (string, int) { // 血量一直为150 hp := 150 // 返回创建的闭包 return func(name string) (string, int) { // 将变量引用到闭包中 return name, hp } } func main() { // 创建一个玩家生成器 generator := playerGen("码神") // 返回玩家的名字和血量 name, hp := generator() // 打印值 fmt.Println(name, hp) generator1 := playerGen1() name1,hp1 := generator1("码神") // 打印值 fmt.Println(name1, hp1) }
4. 延迟调用
> Go语言的 defer 语句会将其后面跟随的语句进行延迟处理
**defer特性:**
1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
**defer的用途:**
1. 关闭文件句柄
2. 锁资源释放
3. 数据库连接释放
**go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。**
1
2
3
4
5
6
7
8package main import "fmt" func main() { var whatever = [5]int{1,2,3,4,5} for i := range whatever { defer fmt.Println(i) } }//输出4,3,2,1,0
看下面的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13package main import ( "log" "time" ) func main() { start := time.Now() log.Printf("开始时间为:%v", start) defer log.Printf("时间差:%v", time.Since(start)) // Now()此时已经copy进去了 //不受这3秒睡眠的影响 time.Sleep(3 * time.Second) log.Printf("函数结束") }
* Go 语言中所有的`函数调用都是传值的`
* 调用 defer 关键字会`立刻拷贝函数中引用的外部参数` ,包括start 和time.Since中的Now
* defer的函数在`压栈的时候也会保存参数的值,并非在执行时取值`。
如何解决上述问题:使用defer fun()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16package main import ( "log" "time" ) 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("函数结束") }
**因为拷贝的是`函数指针`,函数属于引用传递**
在来看一个问题:
1
2
3
4
5
6
7
8
9package main import "fmt" func main() { var whatever = [5]int{1,2,3,4,5} for i,_ := range whatever { //函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成5,所以输出全都是5. defer func() { fmt.Println(i) }() } }
怎么解决:
1
2
3
4
5
6
7
8
9package main import "fmt" func main() { var whatever = [5]int{1,2,3,4,5} for i,_ := range whatever { i := i defer func() { fmt.Println(i) }() } }
5. 异常处理
> Go语言中使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
**panic:**
1. 内置函数
2. 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
3. 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
4. 直到goroutine整个退出,并报告错误
**recover:**
1. 内置函数
2. 用来捕获panic,从而影响应用的行为
> golang 的错误处理流程:当一个函数在执行过程中出现了异常或遇到 panic(),正常语句就会立即终止,然后执行 defer 语句,再报告异常信息,最后退出 goroutine。如果在 defer 中使用了 recover() 函数,则会捕获错误信息,使该错误信息终止报告。
**注意:**
1. 利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2. recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
3. 多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
1
2
3
4
5
6
7
8
9
10
11
12package main func main() { test() } func test() { defer func() { if err := recover(); err != nil { println(err.(string)) // 将 interface{} 转型为具体类型。 } }() panic("panic error!") }
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
1
2func panic(v interface{}) func recover() interface{}
**延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获:**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15package main import "fmt" func test() { defer func() { // defer panic 会打印 fmt.Println(recover()) }() defer func() { panic("defer panic") }() panic("test panic") } func main() { test() }
**如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码被执 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19package main import "fmt" 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 = %dn", z) } func main() { test(2, 1) }
**除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态:
1
2
3type error interface { Error() string }
标准库 `errors.New` 和 `fmt.Errorf `函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main import ( "errors" "fmt" ) 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) } }
**Go实现类似 try catch 的异常处理:**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main import "fmt" 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) }) }
**如何区别使用 panic 和 error 两种方式?**
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
八.结构体
1. 结构体
> Go语言可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
结构体成员也可以称为“字段”,这些字段有以下特性:
- 字段拥有自己的类型和值;
- 字段名必须唯一;
- 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
使用关键字 **type** 可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。
结构体的定义格式如下:
1
2
3
4
5type 类型名 struct { 字段1 字段1类型 字段2 字段2类型 … }
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- struct{}:表示结构体类型,`type 类型名 struct{}`可以理解为将 struct{} 结构体定义为类型名的类型。
- 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
- 字段1类型、字段2类型……:表示结构体各个字段的类型。
示例:
1
2
3
4type Point struct { X int Y int }
颜色的红、绿、蓝 3 个分量可以使用 byte 类型:
1
2
3type Color struct { R, G, B byte }
**结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存**
1.1 实例化
实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。
**基本的实例化形式:**
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。
1
2
3var ins T `T `为结构体类型,`ins `为结构体的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13package main import "fmt" type Point struct { X int Y int } func main() { //使用.来访问结构体的成员变量,结构体成员变量的赋值方法与普通变量一致。 var p Point p.X = 1 p.Y = 2 fmt.Printf("%v,x=%d,y=%d",p,p.X,p.Y ) }
1
2
3
4
5
6
7
8
9
10
11
12
13package main import "fmt" type Point struct { X int Y int } func main() { var p Point //p.X = 1 //p.Y = 2 //如果不赋值 结构体中的变量会使用零值初始化 fmt.Printf("%v,x=%d,y=%d",p,p.X,p.Y ) }
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main import "fmt" type Point struct { X int Y int } func main() { //可以使用 var p = Point{ 1, 2, } fmt.Printf("%v,x=%d,y=%d",p,p.X,p.Y ) }
**创建指针类型的结构体:**
Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。
1ins := new(T)
- T 为类型,可以是结构体、整型、字符串等。
- ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。
下面的例子定义了一个玩家(Player)的结构,玩家拥有名字、生命值和魔法值:
1
2
3
4
5
6
7
8type Player struct{ Name string HealthPoint int MagicPoint int } tank := new(Player) tank.Name = "名字" tank.HealthPoint = 300
new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。
**取结构体的地址实例化:**
在Go语言中,对结构体进行`&`取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:
1ins := &T{}
其中:
- T 表示结构体类型。
- ins 为结构体的实例,类型为 *T,是指针类型。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main import "fmt" 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) }
1.2 匿名结构体
匿名结构体没有类型名称,无须通过 type 关键字定义就可以直接使用。
1
2
3
4
5
6
7
8
9
10
11ins := struct { // 匿名结构体字段定义 字段1 字段类型1 字段2 字段类型2 … }{ // 字段值初始化 初始化字段1: 字段1的值, 初始化字段2: 字段2的值, … }
- 字段1、字段2……:结构体定义的字段名。
- 初始化字段1、初始化字段2……:结构体初始化时的字段名,可选择性地对字段初始化。
- 字段类型1、字段类型2……:结构体定义字段的类型。
- 字段1的值、字段2的值……:结构体初始化字段的初始值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main import ( "fmt" ) // 打印消息类型, 传入匿名结构体 func printMsgType(msg *struct { id int data string }) { // 使用动词%T打印msg的类型 fmt.Printf("%Tn, msg:%v", msg,msg) } func main() { // 实例化一个匿名结构体 msg := &struct { // 定义部分 id int data string }{ // 值初始化部分 1024, "hello", } printMsgType(msg) }
2. 方法
在Go语言中,结构体就像是类的一种`简化形式`,那么类的方法在哪里呢?
在Go语言中有一个概念,它和方法有着同样的名字,并且大体上意思相同,Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。
接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现,如果这样做了就会引发一个编译错误`invalid receiver type…`
接收器也不能是一个指针类型,但是它可以是任何其他允许类型的指针。
**一个类型加上它的方法等价于面向对象中的一个类**
在Go语言中,类型的`代码`和绑定在它上面的`方法`的代码可以`不放置在一起`,它们可以存在不同的源文件中,唯一的要求是它们必须是`同一个包的`。
> 类型 T(或 T)上的所有方法的集合叫做类型 T(或 T)的方法集。
在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中“方法”的概念与其他语言一致,只是Go语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。
**为结构体添加方法:**
> 需求:将物品放入背包
面向对象的写法:
将背包做为一个对象,将物品放入背包的过程作为“方法”
1
2
3
4
5
6
7
8
9
10
11
12
13package main import "fmt" type Bag struct { items []int } func (b *Bag) Insert(itemid int) { //方法,这个和函数不一样,可以理解为,函数加了一个接收器 b.items = append(b.items, itemid) } func main() { b := new(Bag) b.Insert(1001) fmt.Println(b.items) }
**(b*Bag) 表示接收器,即 Insert 作用的对象实例。每个方法只能有一个接收器**
2.1 接收器
接收器的格式如下:
1
2
3func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) { 函数体 }
- 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
- 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:格式与函数定义一致。
接收器根据接收器的类型可以分为`指针接收器`、`非指针接收器`,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
**指针类型的接收器:**
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
由于指针的特性,调用方法时,`修改接收器指针的任意成员变量,在方法结束后,修改都是有效的`。
示例:
使用结构体定义一个属性(Property),为属性添加 SetValue() 方法以封装设置属性的过程,通过属性的 Value() 方法可以重新获得属性的数值,使用属性时,通过 SetValue() 方法的调用,可以达成修改属性值的效果:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main import "fmt" // 定义属性结构 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()) }
**非指针类型的接收器:**
当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但`修改后无效`。
点(Point)使用结构体描述时,为点添加 Add() 方法,这个方法不能修改 Point 的成员 X、Y 变量,而是在计算后返回新的 Point 对象,Point 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package main import ( "fmt" ) // 定义点结构 type Point struct { X int Y int } // 非指针接收器的加方法 func (p Point) Add(other Point) Point { // 成员值与参数相加后返回新的结构 return Point{p.X + other.X, p.Y + other.Y} } func main() { // 初始化点 p1 := Point{1, 1} p2 := Point{2, 2} // 与另外一个点相加 result := p1.Add(p2) // 输出结果 fmt.Println(result) }
**在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针。**
3. 二维矢量模拟玩家移动
在游戏中,一般使用二维矢量保存玩家的位置,使用矢量运算可以计算出玩家移动的位置,本例子中,首先实现二维矢量对象,接着构造玩家对象,最后使用矢量对象和玩家对象共同模拟玩家移动的过程。
**实现二维矢量结构:**
矢量是数学中的概念,二维矢量拥有两个方向的信息,同时可以进行加、减、乘(缩放)、距离、单位化等计算,在计算机中,使用拥有 X 和 Y 两个分量的 Vec2 结构体实现数学中二维向量的概念。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package main import "math" type Vec2 struct { X, Y float32 } // 加 func (v Vec2) Add(other Vec2) Vec2 { return Vec2{ v.X + other.X, v.Y + other.Y, } } // 减 func (v Vec2) Sub(other Vec2) Vec2 { return Vec2{ v.X - other.X, v.Y - other.Y, } } // 乘 缩放或者叫矢量乘法,是对矢量的每个分量乘上缩放比,Scale() 方法传入一个参数同时乘两个分量,表示这个缩放是一个等比缩放 func (v Vec2) Scale(s float32) Vec2 { return Vec2{v.X * s, v.Y * s} } // 距离 计算两个矢量的距离,math.Sqrt() 是开方函数,参数是 float64,在使用时需要转换,返回值也是 float64,需要转换回 float32 func (v Vec2) DistanceTo(other Vec2) float32 { dx := v.X - other.X dy := v.Y - other.Y return float32(math.Sqrt(float64(dx*dx + dy*dy))) } // 矢量单位化 func (v Vec2) Normalize() Vec2 { mag := v.X*v.X + v.Y*v.Y if mag > 0 { oneOverMag := 1 / float32(math.Sqrt(float64(mag))) return Vec2{v.X * oneOverMag, v.Y * oneOverMag} } return Vec2{0, 0} }
**实现玩家对象:**
玩家对象负责存储玩家的当前位置、目标位置和速度,使用 MoveTo() 方法为玩家设定移动的目标,使用 Update() 方法更新玩家位置,在 Update() 方法中,通过一系列的矢量计算获得玩家移动后的新位置。
1. 使用矢量减法,将目标位置(targetPos)减去当前位置(currPos)即可计算出位于两个位置之间的新矢量
2. 使用 Normalize() 方法将方向矢量变为模为 1 的单位化矢量,这里需要将矢量单位化后才能进行后续计算
3. 获得方向后,将单位化方向矢量根据速度进行等比缩放,速度越快,速度数值越大,乘上方向后生成的矢量就越长(模很大)
4. 将缩放后的方向添加到当前位置后形成新的位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38package main type Player struct { currPos Vec2 // 当前位置 targetPos Vec2 // 目标位置 speed float32 // 移动速度 } // 移动到某个点就是设置目标位置 //逻辑层通过这个函数告知玩家要去的目标位置,随后的移动过程由 Update() 方法负责 func (p *Player) MoveTo(v Vec2) { p.targetPos = v } // 获取当前的位置 func (p *Player) Pos() Vec2 { return p.currPos } //判断玩家是否到达目标点,玩家每次移动的半径就是速度(speed),因此,如果与目标点的距离小于速度,表示已经非常靠近目标,可以视为到达目标。 func (p *Player) IsArrived() bool { // 通过计算当前玩家位置与目标位置的距离不超过移动的步长,判断已经到达目标点 return p.currPos.DistanceTo(p.targetPos) < p.speed } // 逻辑更新 func (p *Player) Update() { if !p.IsArrived() { // 计算出当前位置指向目标的朝向 //数学中,两矢量相减将获得指向被减矢量的新矢量 dir := p.targetPos.Sub(p.currPos).Normalize() // 添加速度矢量生成新的位置 newPos := p.currPos.Add(dir.Scale(p.speed)) // 移动完成后,更新当前位置 p.currPos = newPos } } // 创建新玩家 func NewPlayer(speed float32) *Player { return &Player{ speed: speed, } }
**处理移动逻辑:**
将 Player 实例化后,设定玩家移动的最终目标点,之后开始进行移动的过程,这是一个不断更新位置的循环过程,每次检测玩家是否靠近目标点附近,如果还没有到达,则不断地更新位置,让玩家朝着目标点不停的修改当前位置,如下代码所示:
~~~go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31package main import "fmt" func main() { // 实例化玩家对象,并设速度为0.5 p := NewPlayer(0.5) // 让玩家移动到3,1点 p.MoveTo(Vec2{3, 1}) // 如果没有到达就一直循环 for !p.IsArrived() { // 更新玩家位置 p.Update() // 打印每次移动后的玩家位置 fmt.Println(p.Pos()) } fmt.Printf("到达了:%v",p.Pos()) }
4. 给任意类型添加方法
Go语言可以对任何类型添加方法,给一种类型添加方法就像给结构体添加方法一样,因为结构体也是一种类型。
**为基本类型添加方法:**
在Go语言中,使用 type 关键字可以定义出新的自定义类型,之后就可以为自定义类型添加各种方法了。我们习惯于使用面向过程的方式判断一个值是否为 0,例如:
1
2
3if v == 0 { // v等于0 }
如果将 v 当做整型对象,那么判断 v 值就可以增加一个 IsZero() 方法,通过这个方法就可以判断 v 值是否为 0,例如:
1
2
3if v.IsZero() { // v等于0 }
为基本类型添加方法的详细实现流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package main import ( "fmt" ) // 将int定义为MyInt类型 type MyInt int // 为MyInt添加IsZero()方法 func (m MyInt) IsZero() bool { return m == 0 } // 为MyInt添加Add()方法 func (m MyInt) Add(other int) int { return other + int(m) } func main() { var b MyInt fmt.Println(b.IsZero()) b = 1 fmt.Println(b.Add(2)) }
5. 匿名字段
结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型也就是字段的名字。
匿名字段本身可以是一个结构体类型,即结构体可以包含内嵌结构体。
Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package 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: %pn", &m) fmt.Println(m.ToString()) }
类似于重写的功能:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package 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()) }
最后
以上就是清脆歌曲最近收集整理的关于GO语言学习笔记(二)的全部内容,更多相关GO语言学习笔记(二)内容请搜索靠谱客的其他文章。
发表评论 取消回复