概述
六.循环
1. if else
在Go语言中,关键字` if `是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号`{}`括起来的代码块,否则就忽略该代码块继续执行后续的代码。
if condition {
// 条件为真执行
}
**condition 称之为条件表达式或者布尔表达式,执行结果需返回true或false。{ 必须在条件表达式的尾部**
如果存在第二个分支,则可以在上面代码的基础上添加 `else `关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。
x := 5
if x <= 0 {
fmt.Println("为真进入这里")
//go语言格式要求很严,else必须写在}后面
}else{
fmt.Println("为假进入这里")
}
如果存在第三个分支,则可以使用下面这种三个独立分支的形式:
if condition1 {
// condition1 满足 执行
} else if condition2 {
// condition1 不满足 condition2满足 执行
}else {
// condition1和condition2都不满足 执行
}
1.1 特殊写法
if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:
if a := 10; a >5 {
fmt.Println(a)
return
}
这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。
> 在编程中,变量的作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行,但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助。
2. for
> go语言中的循环语句只支持 for 关键字,这个其他语言是不同的。
sum := 0
//i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环
for i := 0; i < 10; i++ {
sum += i
}
第二种写法:
sum := 0
for {
sum++
if sum > 100 {
//break是跳出循环
break
}
}
上述的代码,如果没有break跳出循环,那么其将无限循环**
第三种写法:
n := 10
for n>0 {
n--
fmt.Println(n)
}
结束循环的方式:
1. return —不会执行之后的代码
step := 2
for step > 0 {
step--
fmt.Println(step)
//执行一次就结束了
return
}
//不会执行
fmt.Println("结束之后的语句....")
2. break —会执行之后的
step := 2
for step > 0 {
step--
fmt.Println(step)
//跳出循环,还会继续执行循环外的语句
break
}
//会执行
fmt.Println("结束之后的语句....")
3. painc —报错方式跳出循环,后面的不会执行
step := 2
for step > 0 {
step--
fmt.Println(step)
//报错了,直接结束
panic("出错了")
}
//不会执行
fmt.Println("结束之后的语句....")
4. goto —跳到特定位置
func 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 案例
输出九九乘法表
package 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)
for key, value := range coll {
...
}
**`value `始终为集合中对应索引的`值拷贝`,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值**
遍历map:
m := map[string]int{
"hello": 100,
"world": 200,
}
for key, value := range m {
fmt.Println(key, value)
}
字符串也可以使用for range:
str := "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 语句的语法如下:
switch var1 {
case val1:
...
case val2:
...
default:
...
}
`变量 var1` 可以是任何类型,而 val1 和 val2 则可以是`同类型的任意值`。
类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。
/* 定义局部变量 */
var 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`
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}
注意事项:
1. 加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s == "world":
fmt.Println("world")
}
5. goto
> goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助,使用 goto 语句能简化一些代码的实现过程。
**使用 goto 退出多层循环**
传统写法:
package 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的写法:
package 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 `的代码块上。
package 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 语句后添加`标签`时,表示开始`标签对应的循环`
package 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)。
- 函数可以满足接口。
**函数定义:**
func function_name( [parameter list] ) [return_types] {
函数体
}
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为`实际参数`。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:`返回类型,函数返回一列值`。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
示例:
package 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() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。**
**返回值可以为多个:
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}
1.1 函数做为参数
函数做为一等公民,可以做为参数传递。
func 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
}
**在将函数做为参数的时候,我们可以使用类型定义,将函数定义为类型,这样便于阅读**
// 定义函数类型。
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
//多个返回值 用括号扩起来
func sum(a,b int) (int,int) {
return a,b
}
func main(){
a,b := sum(2,3)
fmt.Println(a,b)
}
package 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. 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
func swap(x, y int) int {
... ...
}
2. 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
package 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(参数列表)(返回参数列表){
函数体
}
示例:
package main
import (
"fmt"
"math"
)
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。
getsqrt := func (shuzi float64) float64 {
aaa := math.Sqrt(float64(shuzi))
fmt.Println(aaa)
return float64(shuzi)
}(12415)
fmt.Println(getsqrt)
3. 闭包
> 所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
> 闭包=函数+引用环境
示例:
// 创建一个玩家生成器, 输入名称, 输出生成器
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功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。**
package 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
看下面的示例:
package 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()
package 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("函数结束")
}
**因为拷贝的是`函数指针`,函数属于引用传递**
在来看一个问题:
package 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) }()
}
}
怎么解决:
package 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 语句会被最先调用。
package main
func main() {
test()
}
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
}
由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。
func panic(v interface{})
func recover() interface{}
**延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获:**
package main
import "fmt"
func test() {
defer func() {
// defer panic 会打印
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
**如果需要保护代码段,可将代码块重构成匿名函数,如此可确保后续代码被执 :
package 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 类型错误对象来表示函数调用状态:
type error interface {
Error() string
}
标准库 `errors.New` 和 `fmt.Errorf `函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。
package 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 的异常处理:**
package 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 定义为自定义类型后,使结构体更便于使用。
结构体的定义格式如下:
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- struct{}:表示结构体类型,`type 类型名 struct{}`可以理解为将 struct{} 结构体定义为类型名的类型。
- 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。
- 字段1类型、字段2类型……:表示结构体各个字段的类型。
示例:
type Point struct {
X int
Y int
}
颜色的红、绿、蓝 3 个分量可以使用 byte 类型:
type Color struct {
R, G, B byte
}
**结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存**
1.1 实例化
实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。
**基本的实例化形式:**
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。
var ins T
`T `为结构体类型,`ins `为结构体的实例。
package 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 )
}
package 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 )
}
package 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 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。
ins := new(T)
- T 为类型,可以是结构体、整型、字符串等。
- ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。
下面的例子定义了一个玩家(Player)的结构,玩家拥有名字、生命值和魔法值:
type Player struct{
Name string
HealthPoint int
MagicPoint int
}
tank := new(Player)
tank.Name = "名字"
tank.HealthPoint = 300
new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。
**取结构体的地址实例化:**
在Go语言中,对结构体进行`&`取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:
ins := &T{}
其中:
- T 表示结构体类型。
- ins 为结构体的实例,类型为 *T,是指针类型。
示例:
package 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 关键字定义就可以直接使用。
ins := struct {
// 匿名结构体字段定义
字段1 字段类型1
字段2 字段类型2
…
}{
// 字段值初始化
初始化字段1: 字段1的值,
初始化字段2: 字段2的值,
…
}
- 字段1、字段2……:结构体定义的字段名。
- 初始化字段1、初始化字段2……:结构体初始化时的字段名,可选择性地对字段初始化。
- 字段类型1、字段类型2……:结构体定义字段的类型。
- 字段1的值、字段2的值……:结构体初始化字段的初始值。
package 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语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。
**为结构体添加方法:**
> 需求:将物品放入背包
面向对象的写法:
将背包做为一个对象,将物品放入背包的过程作为“方法”
package 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 接收器
接收器的格式如下:
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}
- 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名。例如,Socket 类型的接收器变量应该命名为 s,Connector 类型的接收器变量应该命名为 c 等。
- 接收器类型:接收器类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:格式与函数定义一致。
接收器根据接收器的类型可以分为`指针接收器`、`非指针接收器`,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中。
**指针类型的接收器:**
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
由于指针的特性,调用方法时,`修改接收器指针的任意成员变量,在方法结束后,修改都是有效的`。
示例:
使用结构体定义一个属性(Property),为属性添加 SetValue() 方法以封装设置属性的过程,通过属性的 Value() 方法可以重新获得属性的数值,使用属性时,通过 SetValue() 方法的调用,可以达成修改属性值的效果:
package 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 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率:
package 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 结构体实现数学中二维向量的概念。
package 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. 将缩放后的方向添加到当前位置后形成新的位置
package 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
package 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,例如:
if v == 0 {
// v等于0
}
如果将 v 当做整型对象,那么判断 v 值就可以增加一个 IsZero() 方法,通过这个方法就可以判断 v 值是否为 0,例如:
if v.IsZero() {
// v等于0
}
为基本类型添加方法的详细实现流程如下:
package 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语言中,相比较于继承,组合更受青睐。
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: %pn", &m)
fmt.Println(m.ToString())
}
类似于重写的功能:
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())
}
最后
以上就是清脆歌曲为你收集整理的GO语言学习笔记(二)的全部内容,希望文章能够帮你解决GO语言学习笔记(二)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复