–
第10章 第10章和第11章是重点 Go 通过类型别名(alias types)和结构体的形式支持用户自定义类型,或者叫定制类型 组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。 因为 Go 语言中没有类的概念,因此在 Go 中结构体有着更为重要的地位。 10.1 结构体定义的一般方式如下: type identifier struct { field1 type1 field2 type2 ... } type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。 结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。 使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T) t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。 声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。 可以使用点号符给字段赋值:structname.fieldname = value。 同样的,使用点号符可以获取结构体字段的值:structname.fieldname。 在 Go 语言中这叫 选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段: type myStruct struct { i int } var v myStruct // v是结构体类型变量 var p *myStruct // p是指向一个结构体类型变量的指针 v.i p.i 初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下: ms := &struct1{10, 15.5, "Chris"} // 此时ms的类型是 *struct1 或者: var ms struct1 ms = struct1{10, 15.5, "Chris"} 表达式 new(Type) 和 &Type{} 是等价的。 时间间隔(开始和结束时间以秒为单位)是使用结构体的一个典型例子: type Interval struct { start int end int } 初始化方式: intr := Interval{0, 3} (A) intr := Interval{end:5, start:1} (B) intr := Interval{end:5} (C) (A)值必须以字段在结构体定义时的顺序给出,& 不是必须的。 (B)显示了另一种方式,字段名加一个冒号放在值的前面,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉,就像(C)中那样。 10.2 结构体工厂,函数返回结构体指针 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") 10.4 结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记 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) } 10.5 结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。 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) } 10.6 在 Go 语言中,结构体就像是类的一种简化形式 Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。 接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存 在在不同的源文件,唯一的要求是:它们必须是同一个包的。 类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。 定义方法的一般格式如下: func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } 在方法名之前,func 关键字之后的括号中指定 receiver。 如果 recv 是 receiver 的实例,Method1 是它的方法名,那么方法调用遵循传统的 object.name 选择器符号:recv.Method1()。 如果 recv 一个指针,Go 会自动解引用。 如果方法不需要使用 recv 的值,可以用 _ 替换它,比如: func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... } recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字 下面是一个结构体上的简单方法的例子 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()) fmt.Printf("add the param: %d\n", two1.AddToParam(20)) } func (tn *TwoInts) AddThem() int { return tn.a + tn.b } func (tn *TwoInts) AddToParam(param int) int { return tn.a + tn.b + param } =================================== 类型和作用在它上面定义的方法必须在同一个包里定义,可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。 方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。 type IntVector []int func (v IntVector) Sum() (s int) { for _,x := range v { s += x } return } func main() { fmt.Println(IntVector{1,2,3}.Sum()) } 或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。 type myTime struct { time.Time //anonymous field } func (t myTime) first3Chars() string { return t.Time.String()[0:3] } func main() { m := myTime{time.Now()} // 调用匿名Time上的String方法 fmt.Println("Full time now:", m.String()) // 调用myTime.first3Chars fmt.Println("First 3 chars:", m.first3Chars()) } ================================================ 指针方法和值方法都可以在指针或非指针上被调用 鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针 如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法 对于类型 T,如果在 *T 上存在方法 Meth(),并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()。 指针方法和值方法都可以在指针或非指针上被调用,就是形参和实参不用刻意统一类型,会自动转换指针和变量 type List []int func (l List) Len() int { return len(l) } func (l *List) Append(val int) { *l = append(*l, val) } func main() { // 值 var lst List lst.Append(1) fmt.Printf("%v (len: %d)", lst, lst.Len()) // [1] (len: 1) // 指针 plst := new(List) plst.Append(2) fmt.Printf("%v (len: %d)", plst, plst.Len()) // &[2] (len: 1) } 10.6.4 方法和未导出字段 考虑 person2.go 中的 person 包:类型 Person 被明确的导出了,但是它的字段没有被导出。例如在 use_person2.go 中 p.firstName 就是错误的。 该如何在另一个程序中修改或者只是读取一个 Person 的名字呢? 这可以通过面向对象语言一个众所周知的技术来完成:提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。 示例 10.15 person2.go: package person type Person struct { firstName string lastName string } func (p *Person) FirstName() string { return p.firstName } func (p *Person) SetFirstName(newName string) { p.firstName = newName } 示例 10.16—use_person2.go: package main import ( "./person" "fmt" ) func main() { p := new(person.Person) // p.firstName undefined // (cannot refer to unexported field or method firstName) // p.firstName = "Eric" p.SetFirstName("Eric") fmt.Println(p.FirstName()) // Output: Eric } 10.6.5 内嵌类型的方法和继承 模拟经典面向对象语言中的子类和继承相关的效果 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 } 可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法 在示例 10.18 method4.go 中添加: func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. } 现在 fmt.Println(n.Abs()) 会打印 500。 10.6.7 多重继承 多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。因为在类继承层次中,多重继 承会给编译器引入额外的复杂度。但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。 作为一个例子,假设有一个类型 CameraPhone,通过它可以 Call(),也可以 TakeAPicture(),但是第一个方法属于类型 Phone,第二个方法属于类型 Camera。 只要嵌入这两个类型就可以解个问题,如下所示: 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()) } 10.7 一种可阅读性和打印性的输出。如果类型定义了 String() 方法,它会被用在 fmt.Printf() 中生成默认的输出:等同于使用格式化描述符 %v 产生的输出。 还有 fmt.Print() 和 fmt.Println() 也会自动使用 String() 方法。 相当于函数的说明,自己定义一个说明,当调用这个方法的时候显示这个函数作用或者变量内容 当打印%#v,会自动调用定义的String()方法 type TwoInts struct { a int b int } func main() { two1 := new(TwoInts) two1.a = 12 two1.b = 10 fmt.Printf("two1 is: %v\n", two1) fmt.Println("two1 is:", two1) fmt.Printf("two1 is: %T\n", two1) fmt.Printf("two1 is: %#v\n", two1) } func (tn *TwoInts) String() string { return "(" + strconv.Itoa(tn.a) + "/" + strconv.Itoa(tn.b) + ")" } 输出: two1 is: (12/10) two1 is: (12/10) two1 is: *main.TwoInts two1 is: &main.TwoInts{a:12, b:10} 10.8 下面是一个示例(可以在练习 10.8 中进一步学习):假定有一个 Engine 接口类型,一个 Car 结构体类型,它包含一个 Engine 类型的匿名字段: type Engine interface { Start() Stop() } type Car struct { Engine } 我们可以构建如下的代码: func (c *Car) GoToWorkIn() { // get in car c.Start() // drive to work c.Stop() // get out of car }
–
评论前必须登录!
注册