ArcherWong博客
首页
博客
golang笔记---结构(struct)和方法(method)
作者:ArcherWong
分类:golang
时间:2019-01-03 21:34:24
阅读:567
[TOC] # 1.前言 一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new 函数来创建。 组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。 结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type),在一些老的编程语言中叫 记录(Record) 在golang中struct 类似于类的作用,我们知道类有属性和方法,这里结构体定义的时候,只是对类中的属性进行定义赋值,并没有对方法进行定义,但是,方法可以随时定义绑定到该类的对象上,更具灵活性,并且可利用嵌套组合来实现类似继承的功能避免代码重复。 体会下典型例子 ``` type Rect struct{ //定义矩形类 x,y float64 //类型只包含属性,并没有方法 width,height float64 } //为Rect类型绑定Area的方法, //*Rect为指针引用,可以修改传入参数的值,如果为func (r Rect) Area() float64{ ... }则不会 func (r *Rect) Area() float64{ return r.width*r.height //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法 //在Go语言中没有隐藏的this指针,即显示传递,形参即为this,这里形参为r。 } ``` # 2.结构体 ## 定义 一般: ``` type identifier struct { field1 type1 field2 type2 ... } ``` 简单的结构体可以简写。 ``` type T struct {a, b int} ``` **结构体的字段可以是任何类型**,甚至是结构体本身,也可以是函数或者接口。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值: ``` var s T s.a = 5 s.b = 8 ``` **数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段**。 ## 声明和初始化 ### 声明和初始化实例 ``` package main import "fmt" type foo struct { name string age int } func main() { //声明并初始化 var foo1 foo foo1.name = "archer" foo1.age = 18 //声明并使用字面量 var foo2 foo foo2 = foo{"archer", 2} //简短写法 foo3 := foo{"archer", 3} // 混合字面量语法,&foo{"archer", 4}是一种简写,底层仍会调用new() foo4 := &foo{"archer", 4} fmt.Println("foo4:",foo4) //使用new(),表达式 new(Type) 和 &Type{} 是等价的 var foo5 *foo foo5 = new(foo) foo5.name = "archer" foo5.age = 5 fmt.Println("foo5:",foo5) // 声明和初始化在一行 var foo6 *foo = new(foo) foo6.name = "archer" foo6.age = 6 fmt.Println("foo6:",foo6) // 简短写法 foo7 := new(foo) foo7.name = "archer" foo7.age = 7 fmt.Println(foo1, foo2, foo3, foo4, foo5, foo6, foo7) } ``` ``` {archer 18} {archer 2} {archer 3} &{archer 4} &{archer 5} &{archer 6} &{archer 7} ``` - 可以使用点号符给字段赋值:`structname.fieldname = value`。 - 同样的,使用点号符可以获取结构体字段的值:`structname.fieldname`。 - 无论变量是一个结构体类型还是一个结构体类型指针,都可以使用同样的方式获取字段值或给字段赋值: ### 图解初始化 使用new初始化  使用结构体字面量初始化  ## 使用工厂方法创造结构体实例 假设定义了如下的 File 结构体类型: ``` //如果想强制别的包必须使用工厂方法创建实例,根据可见性规则,那么File应该小写 type File struct { fd int // 文件描述符 name string // 文件名 } ``` 下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针: ``` func NewFile(fd int, name string) *File { if fd < 0 { return nil } return &File{fd, name} } ``` 然后这样调用它: ``` f := NewFile(10, "./test.txt") ``` ## 带标签的结构体 结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。 示例 10.7 struct_tag.go: ``` package main import ( "fmt" "reflect" ) type TagType struct { // tags field1 bool "An important answer" field2 string "The name of the thing" field3 int "How much there are" } func main() { tt := TagType{true, "Barak Obama", 1} for i := 0; i < 3; i++ { refTag(tt, i) } } func refTag(tt TagType, ix int) { ttType := reflect.TypeOf(tt) ixField := ttType.Field(ix) fmt.Printf("%v\n", ixField.Tag) } ``` 输出: ``` An important answer The name of the thing How much there are ``` ## 匿名字段和内嵌结构体 ### 实例 结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。 可以粗略地将这个和面向对象语言中的继承概念相比较,它可以用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的。 示例 structs_anonymous_fields.go: ``` package main import "fmt" type innerS struct { in1 int in2 int } type outerS struct { b int c float32 int // anonymous field innerS //anonymous field } func main() { outer := new(outerS) outer.b = 6 outer.c = 7.5 outer.int = 60 outer.in1 = 5 outer.in2 = 10 fmt.Printf("outer.b is: %d\n", outer.b) fmt.Printf("outer.c is: %f\n", outer.c) fmt.Printf("outer.int is: %d\n", outer.int) fmt.Printf("outer.in1 is: %d\n", outer.in1) fmt.Printf("outer.in2 is: %d\n", outer.in2) // 使用结构体字面量 outer2 := outerS{6, 7.5, 60, innerS{5, 10}} fmt.Println("outer2 is:", outer2) } ``` 输出: ``` outer.b is: 6 outer.c is: 7.500000 outer.int is: 60 outer.in1 is: 5 outer.in2 is: 10 outer2 is:{6 7.5 60 {5 10}} ``` 通过类型 outer.int 的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。 ### 命名冲突 当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢? - 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式; - 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。 例子: 现有结构体A 和 B ``` type A struct {a int} type B struct {a, b int} ``` 规则1:使用 d.b 是没问题的:它是 float32,而不是 B 的 b。如果想要内层的 b 可以通过 d.B.b 得到。 ``` type D struct {B; b float32} var d D ``` 规则 2:使用 c.a 是错误的,到底是 c.A.a 还是 c.B.a 呢?会导致编译器错误:ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a。 ``` type C struct {A; B} var c C ``` # 3.方法 ## 简介 在 Go 语言中,结构体就像是类的一种简化形式,一个类型加上它的方法等价于面向对象中的一个类。Go方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此**方法是一种特殊类型的函数**。 接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。 因为方法是函数,所以同样的,**不允许方法重载**,即对于一个类型只能有一个给定名称的方法。但是不同的接收者类型,是可以使用相同名称的: ``` func (a *denseMatrix) Add(b Matrix) Matrix func (a *sparseMatrix) Add(b Matrix) Matrix ``` ## 定义方法 ``` func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } ``` recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字。 示例 method .go: ``` package main import "fmt" type TwoInts struct { a int b int } func main() { two1 := new(TwoInts) two1.a = 12 two1.b = 10 fmt.Printf("The sum is: %d\n", two1.AddThem()) //输出22 } func (tn *TwoInts) AddThem() int { return tn.a + tn.b } ``` ## 指针或值作为接收者区别 ``` package main import ( "fmt" ) type B struct { thing int } //recv(b)是一个指向 receiver_type(B) 的指针 func (b *B) change(num int) { b.thing = num } //receiver_type是普通类型值 func (b B) write() string { return fmt.Sprint(b) } func main() { var b1 B // b1是值 b1.change(1) fmt.Println(b1.write()) b2 := new(B) // b2是指针 b2.change(2) fmt.Println(b2.write()) } ``` 结果 ``` {1} {2} ``` 如果改变change为值方法 ``` func (b B) change(num int) { b.thing = num } ``` 则上面的结果为 ``` {0} {0} ``` - b1和b2分别是值和指针,但是都可以调用change 和 write方法,**指针方法和值方法都可以在指针或非指针上被调用** - 因为change是一个指针方法,所以我们可以在方法内修改实例的属性thing,并且会改变原始实例的值。 - 指针方法和值方法的区别在于,指针方法可以修改recv的值,并且因为是指针传递,传递的代价要比值方法低很多。 ## 内嵌类型的方法和继承 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。 内嵌结构体上的方法可以直接在外层类型的实例上调用: ``` package main import ( "fmt" "math" ) type Point struct { x, y float64 } func (p *Point) Abs() float64 { return math.Sqrt(p.x*p.x + p.y*p.y) } type NamedPoint struct { Point name string } func main() { n := &NamedPoint{Point{3, 4}, "Pythagoras"} fmt.Println(n.Abs()) // 打印5 } ``` NamedPoint里面嵌套着Point,我们可以直接实例化NamePoint之后使用Abs方法。内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法,可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。 上面例子,添加如下方法 ``` func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. } ``` 现在 fmt.Println(n.Abs()) 会打印 500。 ## 多重继承 多重继承指的是类型获得多个父类型行为的能力,在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。 如下所示: ``` package main import ( "fmt" ) type Camera struct{} func (c *Camera) TakeAPicture() string { return "Click" } type Phone struct{} func (p *Phone) Call() string { return "Ring Ring" } type CameraPhone struct { Camera Phone } func main() { cp := new(CameraPhone) fmt.Println("Our new CameraPhone exhibits multiple behaviors...") fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture()) fmt.Println("It works like a Phone too: ", cp.Call()) } ``` 输出: ``` Our new CameraPhone exhibits multiple behaviors... It exhibits behavior of a Camera: Click It works like a Phone too: Ring Ring ``` **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md
标签:
上一篇:
golang笔记---接口(interface)
下一篇:
golang笔记---控制结构
文章分类
css
elasticsearch
git
golang
guacamole
javascript
letsencrypt
linux
nginx
other
php
python
vue
web
阅读排行
golang笔记---关联数组(map)
letsencrypt证书-管理工具certbot
centos7.3配置guacamole
golang笔记---template模板语法
nginx笔记-proxy_cache缓存详解
友情链接
node文件
laravel-vue
ArcherWong的博客园