第7章 使用结构体和指针

7.1 结构体是什么

结构体是一系列具有指定数据类型的数据字段,它能够让您通过单个变量引用一系列相关的值。通过使用结构体,可在单个变量中存储众多类型不同的数据字段。通过使用结构体,可提高模块化程度,还能够让您创建并传递复杂的数据结构。

package main                                                   
                                                               
import (                                                       
    "fmt"                                                      
    "reflect"                                                  
)                                                              
                                                               
type Movie struct {                                            
    Name string                                                
    Rating float32                                             
}                                                              
                                                               
func main() {                                                  
    m := Movie{                                                
        Name: "cityzen",    
        //注意,此处结尾必须有逗号,不然报错!
        Rating: 10,                                            
    }                                                          
                                                               
    fmt.Println(m.Name);                                       
} 
  • 关键字type指定一种新类型。
  • 将新类型的名称指定为Movie。
  • 类型名右边是数据类型,这里为结构体。
  • 在大括号内,使用名称和类型指定了一系列数据字段。请注意,此时没有给数据字段赋值。可将结构体视为模板。
  • 在main函数中,使用简短变量赋值声明并初始化了变量m,给数据字段指定的值为相应的数据类型。
  • 使用点表示法访问数据字段并将其打印到控制台。

7.2 创建结构体

声明结构体后,就可通过多种方式创建它。假设您已经声明了一个结构体,那么就可直接声明这种类型的变量。

要调试或查看结构体的值,可使用fmt包将结构体的字段名和值打印出来。为此,可使用占位符%+v。

type Movie struct {
    Name string
    Rating float32
}

func main() {
    var m Movie
    m.Name = "Sun"
    m.Rating = 9.2
    fmt.Printf("%+v", m);
}

输出

{Name:Sun Rating:9.2}

也可使用关键字new来创建结构体实例

var q = new(Movie)

7.3 嵌套结构体

时候,数据结构需要包含多个层级。此时,虽然可选择使用诸如切片等数据类型,但有时候需要的数据结构更复杂。为建立较复杂的数据结构,在一个结构体中嵌套另一个结构体的方式很有用。

type Hero struct {
    Name string
    Age int
    Adr Address
}

type Address struct{
    Street string
    City string
}

7.4 自定义结构体数据字段的默认值

创建结构体时,如果没有给其数据字段指定值,它们将被设为Go语言中的的零值。Go语言没有提供自定义默认值的内置方法,但可使用构造函数来实现这个目标。构造函数创建结构体,并将没有指定值的数据字段设置为默认值。

type Hero struct {
    Name string
    Age int
}

func NewHero() Hero{
    h := Hero{
        Name : "yx",
        Age : 0,
    }

    return h;
}

func main() {
    var h = NewHero();
    fmt.Printf("%+v\n", h);
}

这里不直接创建结构体Hero,而是使用函数NewHero来创建,从而让字段包含自定义的默认值。请注意,这只是一种技巧,而并非Go语言规范的组成部分。

7.5 比较结构体

对结构体进行比较,要先看它们的类型和值是否相同。对于类型相同的结构体,可使用相等性运算符来比较。要判断两个结构体是否相等,可使用==;要判断它们是否不等,可使用!=。

不能对两个类型不同的结构体进行比较,否则将导致编译错误。

7.6 理解公有和私有值

要导出结构体及其字段,结构体及其字段的名称都必须以大写字母打头。

说明:

  • 这里的导出,指的是不同包(package)之间的导出。
  • 大写的结构体或者字段可以在包外被导出并引用。小写结构体及属性字段只能在本包内引用。

7.7 区分指针引用和值引用

将指向结构体的变量赋给另一个变量时,被称为赋值。

a := b

赋值后,a与b相同,但它是b的副本,而不是指向b的引用。修改b不会影响a,反之亦然。

要修改原始结构体实例包含的值,必须使用指针。指针是指向内存地址的引用,因此使用它操作的不是结构体的副本而是其本身。要获得指针,可在变量名前加上&。

package main

import (
    "fmt"
)

type Movie struct {
    Name string
    Rating float32
}

func main() {
    var m Movie
    m.Name = "Sun"
    m.Rating = 9.2

    var mcopy *Movie = &m;
    mcopy.Name = "lunar"

    fmt.Printf("%+v\n%+v\n", m, *mcopy);
    fmt.Printf("%p\n%p\n", &m, mcopy);
}

输出

{Name:lunar Rating:9.2}
{Name:lunar Rating:9.2}
0xc00000a080
0xc00000a080

mcopy的值,即是m的地址,也就是&m

第8章创建方法和接口

8.1 使用方法

方法类似于函数,但有一点不同:在关键字func后面添加了另一个参数部分,用于接受单个参数。下面的示例给结构体Movie添加了一个方法。

type Movie struct {
    Name string
    Rating float32
}

func (m *Movie) summary() string{
    //code
}

在方法声明中,关键字func后面多了一个参数——接收者。严格地说,方法接收者是一种类型,这里是指向结构体Movie的指针。接下来是方法名、参数以及返回类型。

package main

import (
    "fmt"
    "strconv"
)

type Movie struct {
    Name string
    Rating float64
}

func (m *Movie) summary() string{
    r := strconv.FormatFloat(m.Rating, 'f', 1, 64)
    return m.Name + ", " + r
}

func main() {
    var m = Movie{"Summer", 6.6}
    fmt.Println(m.summary());
}

8.2 创建方法集

方法集是可对特定数据类型进行调用的一组方法。方法集可包含的方法数量不受限制,这是一种封装功能和创建库代码的有效方式。

type Math struct {
    Num int
}

func (m *Math) Square() int{
    return m.Num * m.Num
}

func (m *Math) Cub() int{
    return m.Num * m.Num * m.Num
}

8.3 使用方法和指针

方法是一个接受被称为接收者的特殊参数的函数,接收者可以是指针,也可以是值。

指针和值之间的差别很微妙,但选择使用指针还是值这一点很简单:如果需要修改原始结构体,就使用指针;如果需要操作结构体,但不想修改原始结构体,就使用值。

8.4 使用接口

在Go语言中,接口指定了一个方法集,这是实现模块化的强大方式。您可将接口视为方法集的蓝本,它描述了方法集中的所有方法,但没有实现它们。

下面的接口描述了开关机器人的方式。

type Robot interface{
    PowerOn() error
}

那么如何使用接口呢?接口是方法集的蓝本,要使用接口,必须先实现它。如果代码满足了接口的要求,就实现了接口。

可以像下面这样来实现接口Robot。

type T850 struct{
    Name string
}

func (a *T850) PowerOn() error{
    return nil 
}

接口也是一种类型,可作为参数传递给函数,因此可编写可重用于多个接口实现的函数。例如,编写一个可用于启动任何机器人的函数。

func Boot(r Robot) error{
    return r.PowerOn()
}

这个函数将接口Robot的实现作为参数,并返回调用方法PowerOn的结果。这个函数可用于启动任何机器人,而不管其方法PowerOn是如何实现的。

下面是一个完整的使用接口Robot的示例。

package main

import (
    "fmt"
    "errors"
)

type Robot interface{
    PowerOn() error
}

type T850 struct{
    Name string
}

func (a *T850) PowerOn() error{
    return nil 
}

type R2D2 struct{
    Broken bool
}

func (r *R2D2) PowerOn() error{
    if r.Broken{
        return errors.New("R2D2 is broken")
    }else{
        return nil 
    }   
}

func Boot(r Robot) error{
    return r.PowerOn()
}

func main() {
    var t = T850{"The xb"}
    var r = R2D2{true}
    
    fmt.Println(Boot(&t))
    fmt.Println(Boot(&r))
}

8.6 问与答

问:函数和方法有何不同?

答:严格地说,方法和函数的唯一差别在于,方法多了一个指定接收者的参数,这让您能够对数据类型调用方法,从而提高代码重用性和模块化程度。

第9章使用字符串

9.1 创建字符串字面量

Go语言支持两种创建字符串字面量的方式。解释型字符串字面量是用双引号括起的字符,如"hello"。一种创建字符串的简单方式是使用解释型字符串字面量。

var str = "this is a demo"

除换行符和未转义的双引号外,解释型字符串字面量可包含其他任何字符。对于前面有反斜杠(\)的字符,将像它们出现在rune字面量中那样进行解读。

9.2 理解rune字面量

通过使用rune字面量,可将解释型字符串字面量分成多行,还可在其中包含制表符和其他格式选项。

var str = "this\tis\na\tdemo!"

输出

this    is
a       demo!

原始字符串字面量用反引号括起,如’hello’。不同于解释型字符串,原始字符串中的反斜杠没有特殊含义,Go按原样解释这种字符串。

str := `this   is
a   demo!` 

9.3 拼接字符串

要拼接(合并)字符串,可将运算符+用于字符串变量。还可使用复合赋值运算符+=来拼接字符串。

9.3.1 使用缓冲区拼接字符串

对于简单而少量的拼接,使用运算符+和+=的效果虽然很好,但随着拼接操作次数的增加,这种做法的效率并不高。如果需要在循环中拼接字符串,则使用空的字节缓冲区来拼接的效率更高。

package main

import (
    "bytes"
    "fmt"
)


func main() {
    var buffer bytes.Buffer

    for i:=0; i<500; i++{
        buffer.WriteString("z")
    }

    fmt.Println(buffer.String())
}

9.3.2 理解字符串是什么

要更深入地理解字符串以及如何操作它们,必须首先知道Go语言中的字符串实际上是只读的字节切片。要获悉字符串包含多少个字节,可使用Go语言的内置函数len。

由于Go字符串为字节切片,因此可输出字符串中特定位置的字节值。

    s := "hello";
    fmt.Println(len(s))
    fmt.Println(s[1])

结果为

5
101

输出101,是因为通过索引访问字符串时,访问的是字节而不是字符,因此显示的是以十进制表示的字节值。

在Go语言中,可使用格式设置将十进制值转换为字符和二进制表示。

   s := "hello";
   fmt.Printf("%q", s[1]);//h
   fmt.Printf("%b", s[1]);//1100101

9.3.3 处理字符串

给字符串变量赋值后,就可使用标准库中的strings包提供的任何方法。

1.将字符串转换为小写

ToLower()

strings.ToLower("YX IS YX");

2.在字符串中查找子串

Index()

处理字符串时,另一个常见的任务是在字符串中查找子串。方法Index提供了这样的功能,它接受的第二个参数是要查找的子串。如果找到,就返回第一个子串的索引号;如果没有找到,就返回-1。

strings.Index("qq input is good", "input")//3

3.删除字符串中的空格

TrimSpace()

删除开头和末尾的空格

strings.TrimSpace(" input ")//input