Go chapter5


结构

为了将分散的零件组成一个完整的结构体,go提供了 struct 类型。

struct 允许你将不同类型的东西组合在一起

声明结构

访问结构中字段的值或者为字段赋值都需要用到点标记法,也就是像代码中所示的那样,使用点连接变量名和字段名。

这跟C语言中的结构体很相似。

1
2
3
4
5
6
7
8
9
10
11
var curiosity struct{
lat float64
long float64
}
curiosity.lat=-4.534
curiosity.long=137.434
fmt.Println(curiosity.lat,curiosity.long)
fmt.Println(curiosity)
//运行结果
-4.534 123.434
{-4.534 123.434}

注意:使用 print 类函数可以打印出结构的内容

通过类型复用结构

如果你需要在多个结构中使用同一个字段,那么可以像前面那样,为结构定义相应的类型。

1
2
3
4
5
6
7
8
9
10
11
12
type location struct{
lat float64
long float64
}
func main(){
var sprint location
sprint.lat=23.32
sprint.long=12.34
fmt.Println(sprint)
}
//运行结果
{23.32 12.34}

通过复合字面量初始化结构

在使用复合字面量初始化结构的时候,有两种不同形式可供选择。

如以下代码演示了如何通过成对的字段和值初始化 sprint1 变量和 sprint2 变量,这种形式的初始化可以按任何顺序给定字段,而没有给定的字段则会被初始化为类型对应的零值。

通过成对的字段和值初始化结构的另一个好处是它可以容忍结构发送变化,并在结构添加新字段或是重新排列字段顺序的情况下继续正常工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
func main(){
type location struct{
lat float64
long float64
}
sprint1:=location{lat:-2.34,long:342.344}
fmt.Println(sprint1)
sprint2:=location{long:365.344,lat:-5.34}
fmt.Println(sprint2)
}
//运行结果
{-2.34 342.344}
{-5.34 365.344}

而以下代码,清单中的复合字面量在初始化时并没有给出字段的名称,相反,这种初始化形式要求我们必须按照每个字段在结构中定义的顺序给出相应的值。按顺序给出值的初始化方式只适用于那些不会发生变化并且只包含少量字段的结构类型。

1
2
sprint:=location{-2.34,342.344}
fmt.Println(sprint)

打印struct:%v,打印出结构体数据,%+v,打印出带字段名的数据

1
2
3
4
5
6
sprint:=location{-2.34,342.344}
fmt.Printf("%v\n",sprint)
fmt.Printf("%+v\n",sprint)
//运行结果
{-2.34 342.344}
{lat:-2.34 long:342.344}

结构被复制

sprint2 变量在初始化时复制了 sprintf1 变量包含的值,所以这两个结构发生的变化不会对对方产生任何影响。

1
2
3
4
5
6
7
8
9
10
11
12
func main(){
type location struct{
lat float64
long float64
}
sprint1:=location{-2.34,342.344}
sprint2:=sprint1
sprint2.long+=0.10
fmt.Println(sprint1,sprint2)
}
//运行结果
{-2.34 342.344} {-2.34 342.444}

由结构组成的切片

[]struct用于表示由结构组成的切片,它的独特之处在于,切片包含的每个值都是一个结构而不是像float64这样的基本类型。

结构体组成的切片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main(){
type location struct{
name string
old int
high float64
}
pep:=[]location{
{name:"h",old:18,high:168.5},
{name:"z",old:20,high:172.3},
{name:"w",old:18,high:165.2},
}
fmt.Println(pep)
}
//运行结果
[{h 18 168.5} {z 20 172.3} {w 18 165.2}]

将结构编码为 JSON

JavaScript 对象标识法(JSON)Douglas Crockford 推广的一种数据格式,它原本只是 JavaScript 语言的一个子集,但现在已经得到了其他编程语言的广泛支持。JSON 常常被用于 Web API(应用程序接口)

如下代码所示,来自 json 包的 Marshal 函数将把 location 结构中的数据编码为 json 格式,并以字节形式返回编码后的 json 数据。这些数据既可以通过网络进行传输,也可以转换为字符串以便打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"fmt"
"encoding/json"
"os"
)

func main(){
type location struct{
Lat,Long float64
}
curiosity:=location{-3.323,123.343}
bytes,err:=json.Marshal(curiosity)
if err!=nil{
os.Exit(1)
}
fmt.Println(string(bytes))
}
//运行结果
{"Lat":-3.323,"Long":123.343}

编码得出的 JSON 数据的键与 location 结构的字段名是一一对应的。需要注意的是,Marshal 函数只会对结构中被导出的字段实施编码。换句话说,如果上例中 location 结构的 Lat 字段和 Long 字段都以小写字母开头,那么编码的结构将会是 {}。

使用结构标签定制 JSON

go语言的 json 包要求结构中的字段必须以大写字母开头,并且包含多个单词的字段名称必须使用类似 CemelCase 这样的驼峰命名惯例,但是有时候我们也会想要让 JSON 数据使用类似 snake_case 这样的蛇形命名惯例,特别是在与 Python 或者 Ruby 等语言进行交互的时候更是如此。为了解决这个问题,我们可以对结构中的字段打标签(tag),是 json 包在编码数据的时候能够按照我们的意愿修改字段的名称。

跟前面的代码清单相比,如下代码唯一的修改就是引入了能够改变 Marshal 函数输出结构的结构标签。正如之前所述,Lat 字段和 Long 字段都必须是被导出的字段,这样 json 包才能处理它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"fmt"
"encoding/json"
"os"
)
func main(){
type location struct{
Lat float64 `json:"latitude"`
Long float64 `json:"longitude"`
}
curiosity:=location{-3.323,123.343}
bytes,err:=json.Marshal(curiosity)
if err!=nil{
os.Exit(1)
}
fmt.Println(string(bytes))
}
//运行结果
{"latitude":-3.323,"longitude":123.343}

正如代码清单所示,结构标签实际上就是一段与结构字段相关联的字符串。这里之所以使用 `` 包围的原始字符串字面量而不使用被 ”“ 包围的普通字符串字面量,只是为了省下一些使用反斜杠转义引号的功夫而已。具体来说,如果我们把上例中的结构标签从原始字符串字面量改成普通字符串字面量,那么就需要把它改写成更难读也更麻烦的 “json:\“atirude”” 才行。

结构标签的格式为 key:”value”,其中键的名称通常是某个包的名称。例如,为了定制 Lat 字段在 JSON 编码和 XML 编码时的输出,我们可以将该字段的结构标签设置成 `josn:”latitude”xml:”latitude”`。

另外,正如名称 “结构标签” 所暗示的那样,这一特性只适用于结构中的字段,虽然 josn.Marshal 函数除了能够编码结构,还能够编码其他类型。

go没有类

go和其他经典语言不同,它没有 class,没有对象,也没有继承。

但是go提供了 struct 和方法,通过组合这两者就可以实现面向对象设计的相关概念。

将方法绑定到结构

方法可以被关联到你声明的类型上,所以我们可以将方法关联到结构体类型上以实现类的功能。

要实现这一想法首先要做的就是声明一个类型。

下面例子中定义了一个人结构体,定义了一个方法打印人的名字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type pep struct{
name string
lghi float64
old int
}

func (c pep) p() {
fmt.Println(c.name)
}

func main(){
z:=pep{"张三",170.3,100}
z.p()
}
//运行结果
张三

构造函数

  • 可以使用 struct 复合字面值来初始化你所要的数据
  • 但如果 struct 初始化的时候还要做很多事情,那就可以考虑写一个构造用的函数。
  • go语言没有专用的构造函数,但以 new 或者 New 开头的函数,通常是用来构造数据的。
    1
    2
    3
    4
    5
    6
    type location struct{
    lat,long float64
    }
    func newLocation(lat,long coordinate) location{
    return location{lat.decimal(),long.decimal()}
    }

    New函数

  • 有一些用于构造的函数的名称就是New。
  • 这是因为函数调用时使用 包名.函数名 的形式。
  • 如果该函数叫 NewError,那么调用的时候就是 errors.NewError(),这就不如 errors.New()简介

类的替代品

go语言与 python 等传统语言不一样,它没有提供类,而是通过结构和方法来满足相同的需求。如果我们研究go的这一策略,那么就会发现它跟传统语言做法的区别不大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type world struct{
radius float64
}
var mars = world(radius:3389.5)
func (w world) distance(p1,p2 location) float64{
//代办事项
}
func rad(deg float64) float64{
return deg*math.Pi/180
}
func (w world) distance(p1,p2 location) float64{
s1,c2 :=math.Sincos(rad(p1.lat))
s2,c2 :=math.Sincos(rad(p2.lat))
clong:=math.Cos(rad(p1.long-p2.long))
return w.radius*math.Acos(s1*s2+c1*c2*clong)
}
spirit:=location{-14.5684,175.472636}
opportunity:=location{-1.9462,354.4734}
dist:=mars.distance(spirit,opportunity)
fmt.Printf("%.2f km\n",dist)

组合和转发

  • 在面向对象的世界中,对象由更小的对象组合而成
  • 术语:对象组合或组合
  • go通过结构体实现组合
  • go提供了嵌入特性,它可以实现方法的转发

合并结构

表示多种数据最简单的方法,在结构体中包含多种数据。

1
2
3
4
5
type report struct{
sol int
high,low float64
lat,long float64
}

也可以使用更灵活的通过结构和组合对关联的字段进行分组。
通过例子中从 report 转发至 temperature 的方法,我们能够方便地访问report.average()方法,并且继续使用小型类型构建代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type report struct{
sol int
temperature temper
location location
}
type temperature struct{
high,low celsius
}
type location struct{
lat,long float64
}
type celsius float64

func (t temperature) average() celsius{
return (t.high+low)/2
}

func main(){
fmr.Println("average %v C\n",report.temperature.average())
}

实现自动的转发方法

转发方法能够令方法更易用。
为了避免每次进行转发都要像代码清单那样手动编写方法,那么转发方法将变得相当不便,更别说这些重复的样板代码会给程序带来额外的复杂性了。
好在go语言可以通过结构嵌入实现自动的转发方法。为了将类型嵌入结构,我们只需像代码清单所示的那样,在不给定字段名的情况下指定类型即可。

1
2
3
4
5
6
7
8
9
10
11
type report struct{
sol int
temperature
location
}
report :={
sol : 15,
location:location{-4.5895,137.4417},
temperature:temperature{high:-1.0,low:-78.0},
}
fmr.Printf("average %vo C\n",report.average())

将类型嵌入结构不需要指定字段名,结构会自动为被嵌入的类型生成同名的字段。上面声明的report类型的temperature字段就是一个例子:

1
fmt.Printf("average %vo C\n",report.temperature.average())

嵌入不仅会转发方法,还能够让外部结构直接访问内部结构中的字段。

1
2
3
fmt.Printlf("%vo C\n",report.high)
report.high=32
fmt.Printf("%vo C\n",report.temperature.high)

正如所见,对report.high的修改也将见诸report.temperature.high,这两个字段只是访问相同数据的不同手段而已。

除了结构,我们还可以将任意其他类型嵌入结构。例如,在代码中,虽然sol类型的底层类型只是一个简单的int,但它也跟location和temperature两个结构一样被嵌入了report结构里面。

1
2
3
4
5
6
type sol int
type report struct{
sol
location
temperature
}

在此之后,基于sol类型声明的所有方法都能够通过sol字段或者report类型进行访问。

命名冲突

在下列代码没有进行调用的时候是可以通过编译的。
但是如果进行调用days方法,那么编译器就不会直到所要调用的方法是哪一个方法。

1
2
3
4
5
6
7
func (l location) days(12 location) int{
//代办事项
return 5
}
func (l sol) days(12 sol) int{
return 5
}

接口

  • 接口关注于类型可以做什么,而不是存储了什么
  • 接口通过列举类型必须满足的一组方法来进行声明
  • 在go语言中,不需要显示声明接口

接口类型

类型通过方法表达自己的行为,而接口则通过列举类型必须满足的一组方法来就进行声明。

1
2
3
var t interface{
talk() string
}

任何类型的任何值,只要它满足了接口的要求,就是定义了一个方法返回string类型没有参数,就能够成为变量t的值。具体来说,无论是什么类型,只要它声明的名为talk的方法不接受任何实参并且返回字符串,那么它就满足了接口的要求。

1
2
3
4
5
6
7
8
type martian struct()
func (m martian) talk() string{
return "nack nack"
}
type laser int
func(l laser) talk() string{
return strings.Repeat("pew",int(1))
}

正如如上代码所示,虽然martian类型是一个不包含任何字段的结构,而laser类型则是一个整数,但是由于它们都提供了满足接口要求的talk方法,因此它们都能够被赋值给变量t。

1
2
3
4
5
6
7
var t interface{
talk() string
}
t=martian{}
fmt.Println(t.talk())
t=laser(3)
fmt.Println(t.talk())

具备变形功能的变量t能够采用martian或者laser两种形式。用计算机科学家的话来讲就是接口通过多态让变量t具备了多种形态。

  • 为了复用,通常会把接口声明为类型
  • 按约定,接口名称通常以er结尾
    1
    2
    3
    type talker interface{
    talk() string
    }
    接口类型可以用于在其他类型能够使用的任何地方。
    1
    2
    3
    4
    func shout(t talker){
    louder := strings.ToUpper(t.talk())
    fmt.Println(louder)
    }
    正如代码清单所示,shout函数能够处理任何一个满足talker接口的值,无论它的类型是martian还是laser传递给shout函数的实参必须满足talker接口。

接口在修改代码和扩展代码的时候能够淋漓尽致地发挥其灵活性。例如,如果你声明了一个带有talk方法的新类型,那么shout函数将自动适用于它。此外,无论实现发生何种变化或者新增何种功能,那些只依赖接口的代码都不需要做任何修改。

值得注意的是,接口还可以根结构嵌入特性一同使用,例如如下代码将满足talker接口的laser类型嵌入了starship结构。

1
2
3
4
5
6
7
type starship struct{
laser
}
s:=starship(laser(3))
fmt.Println(s.talk())
fmt.Println(s.talk())
shout(s)

探索接口

  • go语言的接口都是隐式满足的
    go语言允许在实现代码的过程中随时创建新的接口。任何代码都可以实现接口,包括那些已经存在的代码。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package main
    import (
    "fmt"
    "time"
    )
    func stardate(t time.Time) float64{
    doy:=float64(t.YearDay())
    h:=float64(t.Hour())/24.0
    return 100+doy+h
    }
    func main(){
    day:=time.Date(2012,8,6,5,17,0,0,time.UTC)
    fmt.Printf("%.1f Curiosity has landed\n",stardate(day))
    }

满足接口

  • go标准库导出了很多只有单个方法的接口。
  • go通过简单的、通常只有单个方法的接口…..来鼓励组合而不是继承,这些接口在各个组件之间形成了简明易懂的界限。
  • 例如fmt包声明的Stringer接口
    1
    2
    3
    type Stringer interface{
    String() string
    }

后言

参考书籍:Go语言趣学指南
参考课程:Go语言编程快速入门(Golang)