结构与方法
结构体定义
结构体定义的一般方式如下:
type identifier struct {field1 type1field2 type2...
}
type T struct {a, b int}
也是合法的语法,它更适用于简单的结构体。
结构体里的字段都有 名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。
结构体的字段可以是任何类型,甚至是结构体本身,也可以是函数或者接口。
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T)
使用 fmt.Println 打印一个结构体的默认输出可以很好的显示它的内容,类似使用 %v 选项。
就像在面向对象语言所作的那样,可以使用点号符给字段赋值:structname.fieldname = value
。同样的,使用点号符可以获取结构体字段的值:structname.fieldname
在 Go 语言中这叫 选择器(selector)
初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:
ms := &struct1{10, 15.5, "Chris"}
// 此时ms的类型是 *struct1
或者:
var ms struct1
ms = struct1{10, 15.5, "Chris"}
混合字面量语法(composite literal syntax)&struct1{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。
结构体的内存布局
Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。不像 Java 中的引用类型,一个对象和它里面包含的对象可能会在不同的内存空间中,这点和 Go 语言中的指针很像。
递归结构体
结构体类型可以通过引用自身来定义。这在定义链表或二叉树的元素(通常叫节点)时特别有用,此时节点包含指向临近节点的链接(地址)
使用工厂方法创建结构体实例
结构体工厂
Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂” 方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。
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
能获取它。
匿名字段和内嵌结构体
结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。
可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。
package main
import "fmt"
type innerS struct {in1 intin2 int
}
type outerS struct {b intc float32int // anonymous fieldinnerS //anonymous field
}
func main() {outer := new(outerS)outer.b = 6outer.c = 7.5outer.int = 60outer.in1 = 5outer.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.int
的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。
内嵌结构体
同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。外层结构体通过 outer.in1 直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。内层结构体被简单的插入或者内嵌进外层结构体。这个简单的 “继承” 机制提供了一种方式,使得可以从另外一个或一些类型继承部分或全部实现。
package main
import "fmt"
type animal struct {name stringaction stringage int
}
type cat struct {food stringshot stringanimal
}
func main() {cat1 := cat{"fish", "miaomiaomiao", animal{"cat", "jump", 1}}fmt.Printf("the name is %s\n, the age is %d\n, the action is %d\n, the food is %s\n, the shot is %s\n",cat1.name, cat1.age, cat1.action, cat1.food, cat1.shot)cat1.name = "猎鹰"fmt.Printf("the name is %s\n, the age is %d\n, the action is %d\n, the food is %s\n, the shot is %s\n",cat1.name, cat1.age, cat1.action, cat1.food, cat1.shot)
}
命名冲突
当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?
外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式; 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。
方法
方法是什么
在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。
接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型。,因为接口是一个抽象定义,但是方法却是具体实现。接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。
一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法。但是如果基于接收者类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收者类型上存在
func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix
-
func (a *denseMatrix) Add(b Matrix) Matrix
:这是方法的定义。在 Go 语言中,方法的定义以关键字func
开始,后面紧跟着接收者(receiver)和方法名称。在这里,a *denseMatrix
是接收者,表示这个方法属于denseMatrix
类型。接收者的类型是在方法名称之前定义的,用括号括起来。这里的a
是接收者的名称,*denseMatrix
表示这个方法是针对指向denseMatrix
类型的指针的。 -
Add(b Matrix)
:这是方法的签名部分。方法名称是Add
,它接受一个名为b
的参数,参数的类型是Matrix
。在这里,Matrix
可能是一个接口类型,表示这个方法可以接受任何实现了Matrix
接口的类型作为参数。 -
Matrix
:这是方法的返回类型。在 Go 中,方法可以有一个返回类型,这里的Matrix
表示这个方法会返回一个Matrix
类型的结果。
package main
import "fmt"
type TwoInts struct {a intb int
}
func main() {two1 := new(TwoInts)two1.a = 12two1.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
}
类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法。试图在 int 类型上定义方法会得到一个编译错误
但是有一个间接的方式:可以先定义该类型(比如:int 或 float)的别名类型,然后再为别名类型定义方法。或者像下面这样将它作为匿名类型嵌入在一个新的结构体中。当然方法只在这个别名类型上有效。
package main
import ("container/list""fmt"
)
type CustomList struct {*list.List
}
func (p *CustomList) Iter() {for e := p.Front(); e != nil; e = e.Next() {fmt.Println(e.Value)}
}
func main() {lst := &CustomList{list.New()}
// 添加一些元素到列表中lst.PushBack(1)lst.PushBack(2)lst.PushBack(3)
// 使用自定义的 Iter 方法遍历列表lst.Iter()
}
函数和方法的区别
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
方法调用类似于Java中对象调用方法,而函数就是当前类中调用方法
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
指针或值作为接收者
鉴于性能的原因,recv
最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。
如果想要方法改变接收者的数据,就在接收者的指针类型上定义该方法。否则,就在普通的值类型上定义方法。
package main
import "fmt"
type Person struct {Name stringAge int
}
func (p Person) UpdateName(newName string) {p.Name = newName
}
func (p *Person) UpdateAge(newAge int) {p.Age = newAge
}
func main() {person1 := Person{Name: "Alice", Age: 30}person1.UpdateName("Bob")fmt.Println(person1.Name) // 输出: Alice,因为在值类型上定义的方法没有修改原始值
person1.UpdateAge(35)fmt.Println(person1.Age) // 输出: 35,因为在指针类型上定义的方法修改了原始值
}
-
在值类型上定义方法:
-
当你在值类型上定义方法时,方法接收的是该值的副本。这意味着在方法内部对接收者的修改不会影响原始值。
-
这种方式适用于不需要在方法内部修改接收者数据的情况,或者对数据的修改是独立于原始数据的。
-
-
在指针类型上定义方法:
-
当你在指针类型上定义方法时,方法接收的是指向该值的指针。这意味着在方法内部对接收者的修改会影响原始值。
-
这种方式适用于需要在方法内部修改接收者数据的情况,或者对数据的修改需要影响原始数据的情况。
-
指针方法和值方法都可以在指针或非指针上被调用
package main
import ("fmt"
)
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 Listlst.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)
}
方法和未导出字段
使用getter和setter方法获取未导出参数:
package Person
type Person struct {firstName stringsecondName string
}
func (p *Person) FirstName() string {return p.firstName
}
func (p *Person) SetFirstName(name string) {p.firstName = name
}
package main
import ("fmt""goProjects/Person"
)
func main() {p := new(Person.Person)oldName := p.FirstName()fmt.Printf("the old first name is %s\n", oldName)p.SetFirstName("Ye")newName := p.FirstName()fmt.Printf("the new first name is %s", newName)
}
类型的 String() 方法和格式化描述符
当定义了一个有很多方法的类型时,十之八九你会使用 String() 方法来定制类型的字符串形式的输出,换句话说:一种可阅读性和打印性的输出。如果类型定义了 String() 方法,它会被用在 fmt.Printf() 中生成默认的输出:等同于使用格式化描述符 %v 产生的输出。还有 fmt.Print() 和 fmt.Println() 也会自动使用 String() 方法。
package main
import ("fmt""strconv"
)
type TwoInts struct {a intb int
}
func main() {two1 := new(TwoInts)two1.a = 12two1.b = 10fmt.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) + ")"
}
垃圾回收和 SetFinalizer
Go 开发者不需要写代码来释放程序中不再使用的变量和结构占用的内存,在 Go 运行时中有一个独立的进程,即垃圾收集器(GC),会处理这些事情,它搜索不再使用的变量然后释放它们的内存。可以通过 runtime 包访问 GC 进程。
通过调用 runtime.GC() 函数可以显式的触发 GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立即释放一大片内存,此时程序可能会有短时的性能下降(因为 GC 进程在执行)。
学习参考资料:
《Go 入门指南》 | Go 技术论坛 (learnku.com)
Go 语言之旅