GO语言学习笔记(二)

六.循环

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("你的等级是 %s\n", 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 = %d\n", 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("%T\n, 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: %p\n", &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())
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值