我是靠谱客的博主 清脆歌曲,这篇文章主要介绍GO语言学习笔记(二),现在分享给大家,希望可以做个参考。

六.循环

1. if else

在Go语言中,关键字` if `是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号`{}`括起来的代码块,否则就忽略该代码块继续执行后续的代码。
 

复制代码
1
2
3
if condition {     // 条件为真执行 }

**condition 称之为条件表达式或者布尔表达式,执行结果需返回true或false。{ 必须在条件表达式的尾部**

如果存在第二个分支,则可以在上面代码的基础上添加 `else `关键字以及另一代码块,这个代码块中的代码只有在条件不满足时才会执行,if 和 else 后的两个代码块是相互独立的分支,只能执行其中一个。
 

复制代码
1
2
3
4
5
6
7
    x := 5     if x <= 0 {         fmt.Println("为真进入这里")         //go语言格式要求很严,else必须写在}后面     }else{         fmt.Println("为假进入这里")     }

如果存在第三个分支,则可以使用下面这种三个独立分支的形式:

复制代码
1
2
3
4
5
6
7
if condition1 {     // condition1 满足 执行 } else if condition2 {     // condition1 不满足 condition2满足 执行 }else {     // condition1和condition2都不满足 执行 }

1.1 特殊写法

if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:

复制代码
1
2
3
4
if a := 10; a >5 {     fmt.Println(a)     return }

这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在 if、else 语句组合中。

> 在编程中,变量的作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行,但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助。

2. for

> go语言中的循环语句只支持 for 关键字,这个其他语言是不同的。
 

复制代码
1
2
3
4
5
sum := 0 //i := 0; 赋初值,i<10 循环条件 如果为真就继续执行 ;i++ 后置执行 执行后继续循环 for i := 0; i < 10; i++ {     sum += i }

第二种写法:

复制代码
1
2
3
4
5
6
7
8
sum := 0 for {     sum++     if sum > 100 {         //break是跳出循环         break     } }

上述的代码,如果没有break跳出循环,那么其将无限循环**

第三种写法:

复制代码
1
2
3
4
5
n := 10 for n>0 {     n--     fmt.Println(n) }

结束循环的方式:

1. return  —不会执行之后的代码

复制代码
1
2
3
4
5
6
7
8
9
step := 2    for step > 0 {        step--        fmt.Println(step)        //执行一次就结束了        return    }    //不会执行    fmt.Println("结束之后的语句....")

2. break —会执行之后的
 

复制代码
1
2
3
4
5
6
7
8
9
 step := 2    for step > 0 {        step--        fmt.Println(step)        //跳出循环,还会继续执行循环外的语句        break    }    //会执行    fmt.Println("结束之后的语句....")

3. painc —报错方式跳出循环,后面的不会执行

复制代码
1
2
3
4
5
6
7
8
9
   step := 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
15
   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 案例

 输出九九乘法表
 

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
 

复制代码
1
2
3
4
for key, value := range coll {     ... } **`value `始终为集合中对应索引的`值拷贝`,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值**

遍历map:

复制代码
1
2
3
4
5
6
7
m := map[string]int{     "hello": 100,     "world": 200, } for key, value := range m {     fmt.Println(key, value) }

字符串也可以使用for range:

复制代码
1
2
3
4
5
6
    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 语句的语法如下:
 

复制代码
1
2
3
4
5
6
7
8
switch 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
23
  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`

复制代码
1
2
3
4
5
6
7
8
var 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
8
   var 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
23
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的写法:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 `的代码块上。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 语句后添加`标签`时,表示开始`标签对应的循环`

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)。

- 函数可以满足接口。

**函数定义:**

复制代码
1
2
3
func 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
13
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() 函数写在文件的前面,其他函数按照一定逻辑顺序进行编写(例如函数被调用的顺序)。**

**返回值可以为多个:

复制代码
1
2
3
4
5
func 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
14
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 }

**在将函数做为参数的时候,我们可以使用类型定义,将函数定义为类型,这样便于阅读**

复制代码
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
7
func 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
14
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. 值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

复制代码
1
2
3
4
5
   func swap(x, y int) int {           ... ...      }

2. 引用传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 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(参数列表)(返回参数列表){

    函数体

}

示例:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
package main import (     "fmt"     "math" ) func main() {     //这里将一个函数当做一个变量一样的操作。     getSqrt := func(a float64) float64 {         return math.Sqrt(a)     }     fmt.Println(getSqrt(4)) }

在定义时调用匿名函数**

匿名函数可以在声明后调用,例如:

复制代码
1
2
3
4
5
func(data int) {     fmt.Println("hello", data) }(100) //(100),表示对匿名函数进行调用,传递参数为 100。
复制代码
1
2
3
4
5
6
7
8
9
10
11
getsqrt := 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
8
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

看下面的示例:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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()

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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("函数结束") }

**因为拷贝的是`函数指针`,函数属于引用传递**

在来看一个问题:

复制代码
1
2
3
4
5
6
7
8
9
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) }()     } }

怎么解决:

复制代码
1
2
3
4
5
6
7
8
9
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 语句会被最先调用。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
package main func main() {     test() } func test() {     defer func() {         if err := recover(); err != nil {             println(err.(string)) // 将 interface{} 转型为具体类型。         }     }()     panic("panic error!") }

由于 panic、recover 参数类型为 interface{},因此可抛出任何类型对象。

复制代码
1
2
 func panic(v interface{}) func recover() interface{}

**延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获:**

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package 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
19
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 类型错误对象来表示函数调用状态:

复制代码
1
2
3
type 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
23
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 的异常处理:**

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 定义为自定义类型后,使结构体更便于使用。

结构体的定义格式如下:

复制代码
1
2
3
4
5
type 类型名 struct {     字段1 字段1类型     字段2 字段2类型     … }

- 类型名:标识自定义结构体的名称,在同一个包内不能重复。

- struct{}:表示结构体类型,`type 类型名 struct{}`可以理解为将 struct{} 结构体定义为类型名的类型。

- 字段1、字段2……:表示结构体字段名,结构体中的字段名必须唯一。

- 字段1类型、字段2类型……:表示结构体各个字段的类型。

示例:

复制代码
1
2
3
4
type Point struct {     X int     Y int }

颜色的红、绿、蓝 3 个分量可以使用 byte 类型:

复制代码
1
2
3
type Color struct {     R, G, B byte }

**结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存**

1.1 实例化

实例化就是根据结构体定义的格式创建一份与格式一致的内存区域,结构体实例与实例间的内存是完全独立的。

**基本的实例化形式:**

结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。

复制代码
1
2
3
var ins T `T `为结构体类型,`ins `为结构体的实例。
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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 ) }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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 ) }
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体。

复制代码
1
ins := new(T)

- T 为类型,可以是结构体、整型、字符串等。

- ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。

下面的例子定义了一个玩家(Player)的结构,玩家拥有名字、生命值和魔法值:

复制代码
1
2
3
4
5
6
7
8
type Player struct{     Name string     HealthPoint int     MagicPoint int } tank := new(Player) tank.Name = "名字" tank.HealthPoint = 300

new 实例化的结构体实例在成员赋值上与基本实例化的写法一致。

**取结构体的地址实例化:**

在Go语言中,对结构体进行`&`取地址操作时,视为对该类型进行一次 new 的实例化操作,取地址格式如下:

复制代码
1
ins := &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
23
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 关键字定义就可以直接使用。

复制代码
1
2
3
4
5
6
7
8
9
10
11
ins := 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
23
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语言建立的“接收器”强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。

**为结构体添加方法:**

> 需求:将物品放入背包

面向对象的写法:

​   将背包做为一个对象,将物品放入背包的过程作为“方法”

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
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 接收器

接收器的格式如下:

复制代码
1
2
3
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {     函数体 }

- 接收器变量:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 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
23
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 属于小内存对象,在函数返回值的复制过程中可以极大地提高代码运行效率:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 结构体实现数学中二维向量的概念。

复制代码
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
38
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. 将缩放后的方向添加到当前位置后形成新的位置

复制代码
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
38
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

复制代码
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
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,例如:

复制代码
1
2
3
if  v == 0 {     // v等于0 }

如果将 v 当做整型对象,那么判断 v 值就可以增加一个 IsZero() 方法,通过这个方法就可以判断 v 值是否为 0,例如:

复制代码
1
2
3
if  v.IsZero() {     // v等于0 }

为基本类型添加方法的详细实现流程如下:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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语言中,相比较于继承,组合更受青睐。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()) }

类似于重写的功能:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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语言学习笔记(二)内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(63)

评论列表共有 0 条评论

立即
投稿
返回
顶部