【读书笔记&个人心得】第10章:结构体与方法
结构 (struct) 与方法 (method)
一个带属性的结构体试图表示一个现实世界中的实体
一个带属性的结构体试图表示一个现实世界中的实体
结构体也是值类型,因此可以通过 new 函数来创建
组成结构体类型的那些数据称为 字段 (fields)。字段=类型+名字
ADT 抽象数据类型
结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type),在一些老的编程语言中叫 记录 (Record)
结构体定义
type identifier struct {
field1 type1
field2 type2
...
}
简单结构体:
type T struct {a, b int}
报错:type int has no field or method String 的field就是这个field(字段)
字段类型
结构体的字段可以是任何类型,甚至是结构体本身(参考第 10.5 节),也可以是函数或者接口(参考第 11 章)
声明和赋值
var s T
s.a = 5
s.b = 8
使用 new()
// 格式 var t *T = new(T)
var t *T
t = new(T)
实例 (instance) 或对象 (object)
声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型 T 。在这两种方式中,t 通常被称做类型 T 的一个实例 (instance) 或对象 (object)
package main
import "fmt"
func main() {
type Book struct {
name string
price uint16
id string
}
var b1 Book
var b2 *Book = new(Book)
b3 := new(Book)
b1.name = "my book1"
b2.name = "my book2"
b3.name = "my book3"
fmt.Println(b1.name, b2.name, b3.name)
}
结构体打印
使用 fmt.Println() 的默认输出 %v 可以很好的显示它的内容
package main
import "fmt"
func main() {
type Book struct {
name string
price uint16
id string
}
var b1 Book
b1.name = "my book"
fmt.Printf("%v", b1) //{my book 0 }
}
选择器 (selector)
包括:structname.fieldname = value 和 structname.fieldname
无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符 (selector-notation) 来引用结构体的字段,对于指针,没有像 C++ 中那样需要使用 -> 操作符,Go 会自动做这样的转换。也可以通过解指针的方式来设置值:(*pers2).lastName = "Woodward"
package main
import (
"fmt"
"strings"
)
type Person struct {
firstName string
lastName string
}
func upPerson(p *Person) {
p.firstName = strings.ToUpper(p.firstName)
p.lastName = strings.ToUpper(p.lastName)
}
func main() {
// 1-struct as a value type:
var pers1 Person
pers1.firstName = "Chris"
pers1.lastName = "Woodward"
upPerson(&pers1)
fmt.Printf("The name of the person is %s %s\n", pers1.firstName, pers1.lastName)
// 2—struct as a pointer:
pers2 := new(Person)
pers2.firstName = "Chris"
pers2.lastName = "Woodward"
(*pers2).lastName = "Woodward" // 这是合法的
upPerson(pers2)
fmt.Printf("The name of the person is %s %s\n", pers2.firstName, pers2.lastName)
// 3—struct as a literal:
pers3 := &Person{"Chris","Woodward"}
upPerson(pers3)
fmt.Printf("The name of the person is %s %s\n", pers3.firstName, pers3.lastName)
}
快速创建结构体实例
ms := &struct1{10, 15.5, "Chris"}
// 此时 ms 的类型是 *struct1
或
var ms struct1
ms = struct1{10, 15.5, "Chris"}
package main
import "fmt"
func main() {
type struct1 struct {
a int
b float32
c string
}
ms := &struct1{10, 2.5, "c"}
var ms2 struct1
ms2 = struct1{10, 15.5, "Chris"}
fmt.Printf("%v %v", ms, ms2)
}
实际上,这是混合字面量语法 (composite literal syntax), &struct1{a, b, c} 是一种简写,底层仍然会调用 new(),这里值的顺序必须按照字段顺序来写。
new(Type) == &Type{}
表达式 new(Type) 和 &Type{} 是等价的
p:=new(s) pp:&s{a:1,b:""}
虽然两者得到的都是指针,但是,在内存上,p指向一个0值的值内存,pp指向值为{a:1,b:""}的值内存
初始化方式
intr := Interval{0, 3} (A)
intr := Interval{end:5, start:1} (B)
intr := Interval{end:5} (C)
(A)(B)(C)中 & 不是必须的
在 (A) 中,值必须以字段在结构体定义时的顺序给出,& 不是必须的。(B) (C)初始化特定字段,这种情况下值的顺序不必一致,并且某些字段还可以被忽略掉
结构体与导出
结构体类型和字段的命名遵循可见性规则(第 4.2 节),也即是说,一个导出的结构体,可以只导出部分字段
ww 包
package ww
import "fmt"
type S struct {
a string
B string
}
func Say() {
fmt.Println("Hello!")
}
// say_hello.go
package main
import (
"fmt"
"ww"
)
func main() {
var s1 ww.S
s1.B = "B" //不存在s1.a
fmt.Printf("s1: %v\n", s1)
}
结构体的内存布局
Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。
但是,就像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,如果结构体内的字段是指针(如结构体指针),则不一定在一个连续块
递归结构体
结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)
单链表
type Node struct {
data float64
su *Node //结构体指针
}
双向链表
type Node struct {
pr *Node
data float64
su *Node
}
二叉树
二叉树中每个节点最多能链接至两个节点:左节点 (le) 和右节点 (ri),这两个节点本身又可以有左右节点,依次类推。树的顶层节点叫根节点 (root),底层没有子节点的节点叫叶子节点 (leaves),叶子节点的 le 和 ri 指针为 nil 值。在
Go 中可以如下定义二叉树
type Tree struct {
le *Tree
data float64
ri *Tree
}
结构体转换(用type套娃)
Go 中的类型转换遵循严格的规则。当为结构体定义了一个 alias (别名)类型时,此结构体类型和它的 alias 类型都有相同的底层类型,它们可以互相转换,同时需要注意其中非法赋值或转换引起的编译错误。
package main
import "fmt"
type number struct {
f float32
}
type nr number // alias type
func main() {
a := number{5.0}
b := nr{5.0}
// var i float32 = b // compile-error: cannot use b (type nr) as type float32 in assignment
// var i = float32(b) // compile-error: cannot convert b (type nr) to type float32
// var c number = b // compile-error: cannot use b (type nr) as type number in assignment
// needs a conversion:
var c = number(b)
fmt.Println(a, b, c) //{5} {5} {5}
}
练习
定义结构体 Address 和 VCard,后者包含一个人的名字、地址编号、出生日期和图像,试着选择正确的数据类型。构建一个自己的 vcard 并打印它的内容。
package main
import "fmt"
func main() {
type Address struct {
value string
}
type VCard struct {
name string
birthday string
img string
address *Address
}
c := &VCard{"张三", "1981-01-22", "good.jpg", &Address{"China"}}
fmt.Printf("%v %v %s", c, c.address, c.address.value)
}
输出:&{张三 1981-01-22 good.jpg 0xc000010250} &{China} China
使用工厂方法创建结构体实例
结构体工厂
//结构体
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")
size := unsafe.Sizeof(T{})
如果想知道结构体类型 T 的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})
强制使用工厂方法
办法就是使用可见性,大写开头可见,小写开头不可见
matrix package
type matrix struct {
...
}
func NewMatrix(params) *matrix {
m := new(matrix) // 初始化 m
return m
}
package main
import "matrix"
...
wrong := new(matrix.matrix) // 编译失败(matrix 是私有的)
right := matrix.NewMatrix(...) // 实例化 matrix 的唯一方式
值类型用new,引用类型(slices / maps / channels)用make
package main
type Foo map[string]string
type Bar struct {
thingOne string
thingTwo int
}
func main() {
// OK
y := new(Bar)
(*y).thingOne = "hello"
(*y).thingTwo = 1
// NOT OK
z := make(Bar) // 编译错误:cannot make type Bar
(*z).thingOne = "hello"
(*z).thingTwo = 1
// OK
x := make(Foo)
x["x"] = "goodbye"
x["y"] = "world"
// NOT OK
u := new(Foo) // new(Foo) 返回的是一个指向 nil 的指针,它尚未被分配内存
(*u)["x"] = "goodbye" // 运行时错误!! panic: assignment to entry in nil map
(*u)["y"] = "world"
}
带标签的结构体
结构体中的字段除了有名字和类型外,还可以有一个可选的标签 (tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记
只有反射可以获取标签
标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它
如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 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)
}
输出:
An important answer
The name of the thing
How much there are
匿名字段和内嵌结构体
只有类型,没有名字
结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字
匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体
作用
它被用来模拟类似继承的行为。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}}
内嵌结构体
注意,这里是有名结构体的内嵌
package main
import "fmt"
type A struct {
ax, ay int
}
type B struct {
A
bx, by float32
}
type C struct {
a A
bx, by float32
}
func main() {
b := B{A{1, 2}, 3.0, 4.0}
c := C{A{1, 2}, 3.0, 4.0}
fmt.Println(b.ax, b.ay, b.bx, b.by)
fmt.Println(c.a.ax, c.a.ay, c.bx, c.by)
fmt.Println(b.A, c.a)
}
输出
1 2 3 4
1 2 3 4
练习
创建一个结构体,它有一个具名的 float32 字段,2 个匿名字段,类型分别是 int 和 string。通过结构体字面量新建一个结构体实例并打印它的内容。
package main
import "fmt"
type A struct {
f float32
int
string
}
func main() {
a := A{1.3222, 3, "str"}
fmt.Println(a.f, a.int, a.string)
}
命名冲突
当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?
1.外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式
2.如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正
当都是 内层 时的冲突,通过类型区分
package main
import "fmt"
func main() {
type A struct{ a int }
type B struct{ a, b int }
type C struct {
A
B
}
var c C
type D struct {
B
b float32
}
var d D
// fmt.Println(c.a) // ambiguous(模糊的) selector c.a
fmt.Println(c.b, d.b, d.B.b)
}
使用 c.a 是错误的,到底是 c.A.a 还是 c.B.a 呢?
这会导致编译器错误:ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a
使用 d.b 是没问题的:它是 float32,而不是 B 的 b。如果想要内层的 b 可以通过 d.B.b 得到
方法
因此方法是一种特殊类型的函数。方法是作用在接收者 (receiver) 上的一个函数,接收者是某种类型的变量(接收者其实就是方法的归属)
接收者类型
接收者类型可以是(几乎)任何类型:结构体、函数、int/bool/string/数组的别名类型
不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现
最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针
package main
import "fmt"
type myint int
func main() {
var a myint
a = 2
b := new(myint)
fmt.Printf("%d", a.add(*b)) // 2
}
func (i myint) add(a myint) (total myint) {
total = i + a
return
}
在一个包内
类型的代码和绑定在它上面的方法的代码,可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
方法集 method set
类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集 (method set)
方法不允许重载(即名字相同,参数列表返回值部分不同)
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。除非接收者不同。
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 是 receiver 的实例,那么方法调用遵循传统的 recv.Method1()
recv 是一个指针,Go 会自动解引用,仍然可以使用 recv.Method1()
recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字
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())
fmt.Printf("Add them to the param: %d\n", two1.AddToParam(20))
two2 := TwoInts{3, 4}
fmt.Printf("The sum is: %d\n", two2.AddThem())
}
func (tn *TwoInts) AddThem() int {
return tn.a + tn.b
}
func (tn *TwoInts) AddToParam(param int) int {
return tn.a + tn.b + param
}
package main
import "fmt"
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()) // 输出是6
}
练习
定义结构体 employee,它有一个 salary 字段,给这个结构体定义一个方法 giveRaise 来按照指定的百分比增加薪水。
package main
import (
"fmt"
"strconv"
)
type Employee struct {
salary float32
}
func main() {
var a Employee
a.salary = 12000
fmt.Printf("%s", a.giveRaise(0.5))
}
func (e Employee) giveRaise(f float32) (total string) {
total = strconv.FormatFloat(float64(e.salary*(1+f)), 'f', 2, 32)
return
}
为什么不能在 int、float32(64) 或类似这些的类型上定义方法
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float32(64) 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误:
cannot define new methods on non-local type int
通过给类型起别名,然后再为别名类型定义方法,就可以实现类型和方法在同一个包内。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效
package main
import (
"fmt"
"time"
)
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())
}
/* Output:
Full time now: Mon Oct 24 15:34:54 Romance Daylight Time 2011
First 3 chars: Mon
*/
函数和方法的区别
在接收者是指针时,方法可以改变接收者的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)
在 Go 中,(接收者)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收者来建立。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的
接收者--指针或值作为接收者
指针代替副本:鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了
要修改接收者,就传指针
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
在接收者上,Go很宽松
Go 为我们做了探测工作,并自动转换
假设 p3 定义为一个指针:p3 := &Point{ 3, 4, 5} ,可以使用 p3.Abs() 来替代 (*p3).Abs()
相反,值类型two2,可以使用two2.AddThem 替代 (&two2).AddThem()
package main
import (
"fmt"
)
type B struct {
thing int
}
func (b *B) change(a int) { b.thing = a }
func (b B) write() string { return fmt.Sprint(b) }
func main() {
var b1 B // b1 是值
(&b1).change(1)
fmt.Println(b1.write())
b1.change(2)
fmt.Println(b1.write())
b2 := new(B) // b2 是指针
b2.change(3)
fmt.Println((*b2).write())
b2.change(4)
fmt.Println(b2.write())
}
/* 输出:
{1}
{2}
{3}
{4}
*/
方法和未导出字段
因为Go中没有public/private修饰符,因此要使用结构体+方法来提供私有变量操作的安全性
person包
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
}
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
}
并发访问对象
跨线程修改对象,可能会引发错误,解决办法时使用sync包的 锁 或者 goroutines + channels
对象的字段(属性)不应该由 2 个或 2 个以上的不同线程在同一时间去改变。如果在程序发生这种情况,为了安全并发访问,可以使用包 sync(参考第 9.3 节中的方法。在第 14.17 节中我们会通过 goroutines 和 channels 探索另一种方式
内嵌类型的方法和继承
当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这就相当于继承了方法,类似 Ruby 中的混入 (mixin)
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
}
变量和方法的覆盖
自己的字段和方法优先权重 比 继承来的 重
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)
}
func (n *NamedPoint) Abs() float64 {
return n.Point.Abs() * 100.
}
type NamedPoint struct {
Point
x string
}
func main() {
n := &NamedPoint{Point{3, 4}, "Pythagoras"}
fmt.Println(n.Abs()) // 打印 500
fmt.Println(n.x) // 打印 Pythagoras
}
如何在类型中嵌入功能
只有两种方式,字段或内嵌
A:聚合(或组合):包含一个所需功能类型的具名字段。
B:内嵌:内嵌(匿名地)所需功能类型。
使用内嵌的好处是,无论你的内嵌是否仍然内嵌,即套娃好几层,里面那些类型的方法可以直接在外层类型中使用。
因此一个好的策略是创建一些小的、可复用的类型作为一个工具箱,用于 组成域(名词) 类型。
A方式:
package main
import (
"fmt"
)
type Log struct {
msg string
}
type Customer struct {
Name string
log *Log
}
func main() {
c := new(Customer)
c.Name = "Barak Obama"
c.log = new(Log)
c.log.msg = "1 - Yes we can!"
// shorter
c = &Customer{"Barak Obama", &Log{"1 - Yes we can!"}}
// fmt.Println(c) &{Barak Obama 1 - Yes we can!}
c.Log().Add("2 - After me the world will be a better place!")
//fmt.Println(c.log)
fmt.Println(c.Log())
}
func (l *Log) Add(s string) {
l.msg += "\n" + s
}
func (l *Log) String() string {
return l.msg
}
func (c *Customer) Log() *Log {
return c.log
}
B方式
package main
import (
"fmt"
)
type Log struct {
msg string
}
type Customer struct {
Name string
Log
}
func main() {
c := &Customer{"Barak Obama", Log{"1 - Yes we can!"}}
c.Add("2 - After me the world will be a better place!")
fmt.Println(c)
}
func (l *Log) Add(s string) {
l.msg += "\n" + s
}
func (l *Log) String() string {
return l.msg
}
func (c *Customer) String() string {
return c.Name + "\nLog:" + fmt.Sprintln(c.Log.String())
}
套娃:
package main
import "fmt"
type A struct {
a int
}
type B struct {
b int
A
}
type C struct {
c int
B
}
func (a *A) funcA() (da int) {
da = 2 * a.a
return
}
func (b *B) funcB() (db int) {
db = 2 * b.b
return
}
func (c *C) funcC() (dc int) {
dc = 2 * c.c
return
}
func main() {
var c C
fmt.Println(c.funcA(), c.funcB(), c.funcC())
}
多重继承
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())
}
会存在菱形继承问题
package main
import (
"fmt"
)
type Camera struct{}
func (c *Camera) TakeAPicture() string {
return "Click"
}
type Phone struct{}
func (p *Phone) TakeAPicture() int {
return 1
}
type CameraPhone struct {
Camera
Phone
}
func main() {
cp := new(CameraPhone)
fmt.Println(cp.TakeAPicture()) // ambiguous selector cp.TakeAPicture
}
练习
定义一个结构体类型 Base,它包含一个字段 id,方法 Id() 返回 id,方法 SetId() 修改 id。结构体类型 Person 包含 Base,及 FirstName 和 LastName 字段。结构体类型 Employee 包含一个 Person 和 salary 字段。
创建一个 employee 实例,然后显示它的 id。
package main
import (
"fmt"
)
type Base struct {
id string
}
func (x *Base) Id() string {
return x.id
}
func (x *Base) SetId(s string) {
x.id = s
}
type Person struct {
Base
FirstName string
LastName string
}
type Employee struct {
Person
salary float32
}
func main() {
p := Employee{Person{Base{"123"}, "Zhang", "san"}, 18000}
fmt.Println(p.Id()) // 123
}
预测结果
package main
import (
"fmt"
)
type Base struct{}
func (Base) Magic() {
fmt.Println("base magic")
}
func (self Base) MoreMagic() {
self.Magic()
self.Magic()
}
type Voodoo struct {
Base
}
func (Voodoo) Magic() {
fmt.Println("voodoo magic")
}
func main() {
v := new(Voodoo)
v.Magic()// 同名冲突,自己的方法优先
v.MoreMagic()
}
a)假设定义: type Integer int,完成 get() 方法的方法体: func (p Integer) get() int { ... }。
b)定义: func f(i int) {}; var v Integer ,如何就 v 作为参数调用f?
c)假设 Integer 定义为 type Integer struct {n int},完成 get() 方法的方法体:func (p Integer) get() int { ... }。
d)对于新定义的 Integer,和 b)中同样的问题。
package main
import "fmt"
type Integer int
type Integer2 struct{ n int }
func main() {
var v Integer
v = 2
var v2 Integer2
v2.n = 4
fmt.Println(v.get())
f(int(v))
fmt.Println(v2.get2())
}
func (p Integer) get() int {
return int(p)
}
func (p Integer2) get2() int {
return int(p.n)
}
func f(i int) {
fmt.Println(i)
}
通用方法和方法命名
类型操作都会使用类型命名,比如打开 (Open)、关闭 (Close)、读 (Read)、写 (Write)、排序(Sort) 等
在 Go 语言中,通过使用接口(参考第 11 章),标准库广泛的应用了这些规则,在标准库中这些通用方法都有一致的名字,比如 Open()、Read()、Write()等。想写规范的 Go 程序,就应该遵守这些约定,给方法合适的名字和签名,就像那些通用方法那样
比如:如果需要一个 convert-to-string() 方法,应该命名为 String(),而不是 ToString()(参考第 10.7 节)。
和其他面向对象语言比较 Go 的类型和方法
在 Java、C++、C# 中,方法需要一个显式的"类"定义,但是在类中,方法绑定在 类型 上,是隐式定义的,类型可以是结构体或者任何用户自定义类型
比如:我们想定义自己的 Integer 类型,并添加一些类似转换成字符串的方法
type Integer int
func (i *Integer) String() string {
return strconv.Itoa(int(*i))
}
在 Java 或 C# 中,这个方法需要和类 Integer 的定义放在一起,在 Ruby 中可以直接在基本类型 int 上定义这个方法。
总结
在 Go 中,类型就是类(数据和关联的方法)
继承有两个好处:代码复用和多态。
PS:多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
在 Go 中,代码复用通过组合和委托实现,多态通过接口的使用来实现:有时这也叫 组件编程 (Component Programming)。
类型的 String() 方法和格式化描述符
------这段可能已经过时-------
如果类型定义了 String() 方法,它会被用在 fmt.Printf() 中生成默认的输出:等同于使用格式化描述符 %v 产生的输出。还有 fmt.Print() 和 fmt.Println() 也会自动使用 String() 方法。
还有 fmt.Print() 和 fmt.Println() 也会自动使用 String() 方法。
格式化描述符
格式化描述符 %T 会给出类型的完全规格,%#v 会给出实例的完整输出,包括它的字段(在程序自动生成 Go 代码时也很有用)
%T :类型
%#v :实例
package main
import (
"fmt"
"strconv"
)
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}
练习
给定结构体类型 T
type T struct {
a int
b float32
c string
}
值 t: t := &T{7, -2.35, "abc\tdef"}。给 T 定义 String(),使得 fmt.Printf("%v\n", t) 输出:7 / -2.350000 / "abc\tdef"
package main
import (
"fmt"
"strconv"
)
type T struct {
a int
b float32
c string
}
func main() {
t := &T{7, -2.35, "abc\tdef"}
t.String()
}
func (t *T) String() {
tt := strconv.FormatFloat(float64(t.b), 'f', 6, 32)
fmt.Printf("%d %s %s", t.a, tt, t.c)
}
为 int 定义一个别名类型 Day,定义一个字符串数组它包含一周七天的名字,为类型 Day 定义 String() 方法,它输出星期几的名字。使用 iota 定义一个枚举常量用于表示一周的中每天(MO、TU...)
package main
import (
"fmt"
)
type Day int
var Weeks = [...]string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
const (
M = iota
Tu
W
Th
F
Sat
Sun
)
func main() {
var day Day
day = Tu
day.String()
}
func (t Day) String() {
fmt.Printf("%s", Weeks[t])
}
为 int 定义别名类型 TZ,定义一些常量表示时区,比如 UTC,定义一个 map,它将时区的缩写映射为它的全称,比如:UTC -> "Universal Greenwich time"。为类型 TZ 定义 String() 方法,它输出时区的全称
package main
import (
"fmt"
)
type TZ int
func main() {
var UTC TZ = 1
UTC.String()
}
func (t TZ) String() {
m := map[int]string{1: "Universal Greenwich time"}
fmt.Printf("%s", m[int(t)])
}
实现栈 (stack) 数据结构
参考资料:https://blog.csdn.net/chengqiuming/article/details/117395972
package main
import (
"errors"
"fmt"
)
// 使用数组来模拟一个栈
type Stack struct {
MaxTop int // 栈最大可以存放数个数
Top int // 栈顶, 因为栈顶固定,因此我们直接使用 Top
arr [5]int // 数组模拟栈
}
// 入栈
func (this *Stack) Push(val int) (err error) {
// 判断栈是否满
if this.Top == this.MaxTop-1 {
fmt.Println("stack full")
return errors.New("stack full")
}
this.Top++
// 放入数据
this.arr[this.Top] = val
return
}
// 出栈
func (this *Stack) Pop() (val int, err error) {
// 判断栈是否空
if this.Top == -1 {
fmt.Println("stack empty!")
return 0, errors.New("stack empty")
}
// 先取值,再 this.Top--
val = this.arr[this.Top]
this.Top--
return val, nil
}
// 遍历栈,注意需要从栈顶开始遍历
func (this *Stack) List() {
// 先判断栈是否为空
if this.Top == -1 {
fmt.Println("stack empty")
return
}
fmt.Println("栈的情况如下:")
for i := this.Top; i >= 0; i-- {
fmt.Printf("arr[%d]=%d\n", i, this.arr[i])
}
}
func main() {
stack := &Stack{
MaxTop: 5, // 最多存放5个数到栈中
Top: -1, // 当栈顶为-1,表示栈为空
}
// 入栈
stack.Push(1)
stack.Push(2)
stack.Push(3)
stack.Push(4)
stack.Push(5)
// 显示
stack.List()
val, _ := stack.Pop()
fmt.Println("出栈val=", val) // 5
// 显示
stack.List() //
fmt.Println()
val, _ = stack.Pop()
val, _ = stack.Pop()
val, _ = stack.Pop()
val, _ = stack.Pop()
val, _ = stack.Pop() // 出错
fmt.Println("出栈val=", val) // 5
// 显示
stack.List()
}
本文作者实现,支持任意类型
// stack.go
package stack
import "errors"
type Stack []interface{}
func (stack Stack) Len() int {
return len(stack)
}
func (stack Stack) Cap() int {
return cap(stack)
}
func (stack Stack) IsEmpty() bool {
return len(stack) == 0
}
func (stack *Stack) Push(e interface{}) {
*stack = append(*stack, e)
}
func (stack Stack) Top() (interface{}, error) {
if len(stack) == 0 {
return nil, errors.New("stack is empty")
}
return stack[len(stack)-1], nil
}
func (stack *Stack) Pop() (interface{}, error) {
stk := *stack // dereference to a local variable stk
if len(stk) == 0 {
return nil, errors.New("stack is empty")
}
top := stk[len(stk)-1]
*stack = stk[:len(stk)-1] // shrink the stack
return top, nil
}
package main
import (
"fmt"
"ww/stack"
)
func main() {
s := stack.Stack{1, 2, 3, 4, 5}
fmt.Println(s.Len())
}
垃圾回收和 SetFinalizer
Go 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器 (GC:Garbage Collection),会处理这些事情,它搜索不再使用的变量然后释放它们的内存。可以通过 runtime 包访问 GC 进程。
通过调用 runtime.GC() 函数可以显式的触发 GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为 GC 进程在执行)。
// Kb就是KB
package main
import (
"fmt"
"runtime"
)
func main() {
// fmt.Printf("%d\n", runtime.MemStats.Alloc/1024)
// 此处代码在 Go 1.5.1下不再有效,更正为
var m runtime.MemStats
runtime.ReadMemStats(&m)// 给出已分配内存的总量
fmt.Printf("%d Kb\n", m.Alloc/1024)
}
对象销毁前执行操作
如果需要在一个对象 obj(某种类型的引用对象) 被从内存移除前执行一些特殊操作,比如写到日志文件中,可以通过如下方式调用函数来实现
runtime.SetFinalizer(obj, func(obj *typeObj))
func(obj *typeObj) 需要一个 typeObj 类型的指针参数 obj,特殊操作会在它上面执行。func 也可以是一个匿名函数。
在对象被 GC 进程选中并从内存中移除以前,SetFinalizer 都不会执行,即使程序正常结束或者发生错误
package main
import (
"fmt"
"runtime"
"time"
)
type object int
func (o object) Ptr() *object {
return &o
}
var (
cacheData = make(map[string]*object, 1024)
)
func deleteData(key string) {
delete(cacheData, key)
fmt.Println(time.Now())
}
func setData(key string, v object) {
data := v.Ptr() //返回指针
runtime.SetFinalizer(data, func(data *object) {
fmt.Printf("runtime invoke Finalizer data: %d, time: %s\n", *data, time.Now().Format("15:04:05.000"))
time.Sleep(time.Second)
})
cacheData[key] = data
}
func main() {
setData("key1", 1)
setData("key2", 2)
setData("key3", 3)
deleteData("key1")
deleteData("key2")
deleteData("key3")
for x := 0; x < 5; x++ {
fmt.Println("invoke runtime.GC()")
runtime.GC() //显式触发GC
time.Sleep(time.Second)
}
}