《The way to go笔记》 第十章 结构与方法

5/22/2023 Golang

# 10.1 结构体的定义和初始化

# 10.1.1 定义

type obj struct {
  key1 type1
  key2 type2
  ...
}

字段可以是任何类型,甚至可以包括结构体本身、函数或接口

# 10.1.2 初始化

结构体初始化有多种方式

type Obj struct {
  name  string
  id    string
  age   int
}
o2 := Obj{ "noxone", "1007", 18 } // 按字段定义顺序赋值
o3 := Obj{ name: "noxone", id: "1007", age: 18 } // 字段显式赋值
o4 := Obj { name: "noxone" } // 其余字段为对应类型零值

// 以下返回的是指针
o5 := new(Obj)
o5.name = "noxone" // &{ name:"noxone", id: "", age: 0 }
o6 := &Obj{ name: "noxone", id: "1007", age: 18 } // &{ name: "noxone", id: "1007", age: 18 }

# 10.1.3 结构体内存连续

Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势!

type Rect1 struct {Min, Max Point }
type Rect2 struct {Min, Max *Point }

image.png 由于结构体内数据在内存中是连续的,所以定义一个链表结构、二叉树结构也从根本上实现了内存连续!!!

// 链表节点
type LinkNode struct {
  pre   *LinkNode
  val   float64
  next  *LinkNode
}

// 二叉树节点
type TreeNode struct {
  left  *TreeNode
  val   float64
  next  *TreeNode
}

# 10.1.4 结构体别名

type Person struct {
  name  string
}

type People Person // 定义Person别名

p1 := Person{ "jack" }
p2 := People{ "jack" }

// People转Person
// var p3 Person = P2  error
p3 := Person(p2)

# 10.2 工厂模式创建结构体实例

Go 语言不像其他面向对象编程语言中那样可以使用构造子方法(即new Obj(...))来创建实例,因此常用的是替代性的使用工厂模式来弥补这一短板

func NewObj(name string, age int) *Obj {
  return &Obj{ name, age }
}

# 10.3 结构体标签

结构体中的字段除了keytype之外,还有一个可选的标签tag:它是一个附属于字段的字符串,在后续可以通过泛型reflect.TypeOf(obj).Field(idx).Tag拿到对应下标的tag

type Obj struct {
  key1 int `tag1`
  key2 int `tag2`
}

func main() {
  obj := Obj{ 1, 2 }
  objType := reflect.TypeOf(obj)
  
  for i := 0; i < 2; i++ {
    ixField := objType.Field(i)
    fmt.Println(ixField.Tag) // tag1 tag2
  }
}

# 10.4 内嵌结构体

结构体可以内嵌结构体或者其他值类型,作为匿名字段。在Go中,继承是通过内嵌或组合来实现

type Person struct {
  name  string
}
type SuperMan struct {
  id  int
  Person // 内嵌结构体
  bool // 内嵌值类型
}
func main() {
  s := new(SuperMan)
  s.id = 107
  s.name = "noxone"
  s.bool = true
  
  fmt.Println(s) // &{ id:107, Person:{ name: "noxone" } bool:true }
  fmt.Println(s.name) // noxone 实际上为 s.Person.name,类似js的原型链查找

  // 以下是使用结构体字面量定义
  s2 := SuperMan{
    id: 107,
    Person: Person{
      name: "noxone"
    },
    bool: true,
  }
  s3 := SuperMan{107, Person{"noxone"}, true}
  
  fmt.Println(s2) // { id:107, Person:{ name: "noxone" } bool:true }
  fmt.Println(s3) // { id:107, Person:{ name: "noxone" } bool:true }
}

在一个结构体中,当字段相同时,外层会覆盖内层,同层字段相同则会报错(不使用的时候不报错!)

// 外层会覆盖内层
type A struct { a int }
type C struct {
  A
  a  int
}
func main() {
  c := C{ a: 10 } // { a: 10, A: {a:0} }
  fmt.Println(c.a) // 10
  fmt.Println(c.A.a) // 0
}
// 同层字段相同报错
type A struct {a int}
type B struct {a, b int}
type C struct {
  A 
  B
}

func main() {
  c := C{} // { A: {a:0}, B: {a:0, b:0} }
  fmt.Println(c.a) // ambiguous selector c.a 摸棱两可的c.a(可能是c.A.a或c.B.a)
}

# 10.5 方法

# 10.5.1 定义

在Go中,方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量,定义方式如下

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

其中recv是接收者(可以立即为JS中的this),receiver_type是接收者的类型,剩余的是定义的方法,在方法中可以操作recv,从而访问到recv里边的字段

type Obj struct {
  name  string
}
func (this Obj) getName() string { // 这里的this并非关键字,只是单纯的变量名,你也可以命名为self,怎么舒服怎么来
  return this.name
}
func (this *Obj) setName(newName) {
  this.name = newName
}

# 10.5.2 接收者按值传递和按指针传递

一般修改实例属性的方法都要指定传入指针而非值,否则修改不成功

按值传递

type Obj struct {
  name  string
}
func (this Obj) setName(newName) {
  this.name = newName
}

func main() {
  o := Obj{"a"}
  o.setName("b")
  fmt.Println(o.name) // a,改了个寂寞
}

按指针传递

type Obj struct {
  name  string
}
func (this *Obj) setName(newName) {
  this.name = newName
}

func main() {
  o := Obj{"a"}
  o.setName("b")
  fmt.Println(o.name) // b
}

# 10.5.3 getter/setter

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.5.4 方法继承

在go中,实例可以继承内嵌结构体实例的方法

type Engine struct {
  v float64
}

type Car struct {
  Engine
  name string
}
func (this *Engine) getV() {
  fmt.Println("引擎速度为: ", this.v)
}

func main () {
  car := &Car{ Engine{100}, "BMW" }
  car.getV() // 引擎速度为:100
}

在自身定义方法,可以覆盖继承的同名方法

func (this *Car) getV() { // 覆盖基类
  fmt.Println(this.name, "的速度为: ", this.v + 50)
}

func main () {
  car := &Car{ Engine{100}, "BMW" }
  car.getV() // BMW的速度为:150
}

对于方法的继承,具体分为组合内嵌,区别是什么呢?不多bb,上代码

组合

type A struct {}
type B struct {}
type C struct {
  id  int
  a   *A
  b   *B
}
func (this *A) fnA() { fmt.Println("a") }
func (this *B) fnB() { fmt.Println("b") }

func main() {
  c := new(C)
  c.a.fnA() // a
  c.b.fnB() // b
  
  // 下面这样不行!
  c.fnA() // c.fnA undefined (type *C has no field or method fnA)
}

内嵌

内嵌也是默认常用的实现继承的方式

type A struct {}
type B struct {}
type C struct {
  id  int
  A
  B
}
func (this *A) fnA() { fmt.Println("a") }
func (this *B) fnB() { fmt.Println("b") }

func main() {
  c := new(C)
  c.fnA() // a   
  c.fnB() // b
  
  // 下面这样也OK,实际上和JS的原型链查找类似
  c.A.fnA() // a
  c.B.fnB() // b
}

# 10.5.5 多重继承

Go实现继承相比其他语言来说比较奔放,js老弟是通过原型即prototype,java老哥则是class B extend A

不管js还是java,继承的本质上都是一个儿子一个爹

而Go就像吕布那样奔放,一个儿子多个爹!!!

但是要注意的是父类之间的方法和属性名千万别重合,否则报形如ambiguous selector o.fn的错

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)
  cp.TakeAPicture()
  cp.Call() 
}

# 10.5.6 String()方法和格式化描述符

在调用fmt.Println(o)时,或其他尝试获取o的字符串操作时,会自动调用oString()方法,和JS里的toString()一毛一样

type Obj struct{
  name  string
}
func (this Obj) String() string {
  return "hello " + this.name
}

o := Obj { "noxone" }
fmt.Println(o) // hello noxone
fmt.Printf("%v", o) // hello noxone
fmt.Printf("%T", o) // main.Obj
fmt.Printf("%#v", o) // main.Obj{name: "noxone"}
    我想,
    在这个世界上,
    虽然没有最美好的相遇,
    但却应该有为了相遇或重逢,
    所做的最美好的努力。
    红莲华
    x
    loading...