结构体
简单介绍 struct
type People struct {
Addr string
name string
year int
}
- Peopel 首字母大写,根据我们所学的首字母大写可导出的知识,它是包级可导出结构
- Addr 首字母大写,所以它是可导出字段,name 和 year 都是小写,所以他们俩不可导出
下面看一下结构体的使用
// 初始化
func main(){
var p People
p.Addr = "北京"
p.name = "小明"
p.year = 2000
var p1 = People{
Addr: "北京",
name: "小明",
year: 2000,
}
var p2 = &People{
Addr: "北京",
name: "小明",
year: 2000,
}
}
type People struct {
Addr string
name string
year int
}
// 结构体的调用
func main(){
var p = &People{
Addr: "北京",
name: "小明",
year: 2000,
}
// 这里是语法糖,p虽然是people的指针,
// 但是它却可以直接调用 Addr
// 实际上就是 (*p).Addr 的省略
p.Addr = "上海"
p.name = "小红"
p.year = 2001
fmt.Println(p.Addr, p.name, p.year)
// 我们还可以使用new来代替&
// 这种写法跟上文中的 var p = &People{}
// 一个意思
var p1 = new(People)
fmt.Println(p1)
}
type People struct {
Addr string
name string
year int
}
解耦结构体声明和调用
当我们实现结构体的时候,如果显示写出结构体的字段变量名称,就可以不按照顺序,以及可以不完全实现全部字段,这样的话,结构体的声明和实现就可以完全解耦,当然可以隐藏实现的结构体变量,那么你不得不要按照顺序,以及实现全部字段,满足这两者才可以。
// 显式实现
type People struct {
Addr string
name string
year func(int)int
}
func main(){
var p = People{
Addr: "北京",
year: func(a int)int{
return 2000 + a
}
}
}
上述代码就是显示的写出了字段的变量名称,你看,name 并没有被写上,这种情况下,name 就会被命名为一个初始值,即 “”
这样,即使结构体本身有什么增加字段的行为,实现结构体的逻辑代码也不用改变了。
如果是隐式的话,那么必须按照顺序,以及数量进行实现,建议在字段不变以及字段数量非常少的时候使用。
匿名 struct
我们使用匿名 struct,可以完全将另一个结构体嵌入到这个结构体中。
type Student struct{
People
score int
}
type People struct {
name string
year string
addr string
}
使用匿名函数,等于将这个匿名函数的字段完全给予了这个新的结构体,这跟使用这个结构体当作一个字段的类型是不同的,我们看一个例子
type Student struct{
People People
score int
}
type People struct {
name string
year string
addr string
}
这段代码跟上文看起来很像,但是一个是将 people 直接嵌入,一个是当成了它的一个字段类型。让我们看一下这两者使用起来的区别
type Student1 struct{
People People
score int
}
type Student struct{
People
score int
}
type People struct {
name string
year string
addr string
}
func main(){
var s = new(Student)
var s1 = new(Student1)
s.name = "小明"
s.year = "2000"
s.addr = "北京"
s.score = 100
fmt.Println(s.name, s.year, s.addr, s.score)
s1.people.name = "小红"
s1.people.year = "2001"
s1.people.addr = "上海"
s1.score = 200
fmt.Println(s1.people.name, s1.people.year, s1.people.addr, s1.score)
}
我们发现匿名函数的字段直接赋予了新的结构体,它不再需要显式的调用 s1.People.name
实际上,不止一个 struct 可以当作匿名函数,内置类型也是可以的
type Student struct{
string
People
int
score int
}
type People struct {
name string
year string
addr string
}
func main(){
var s = new(Student)
s.People = People{
name: "小明",
year: "2000",
addr: "北京",
}
s.int = 12
s.string = "12"
s.score = 100
fmt.Println(s)
}
函数也可以将指针类型当作类型以及直接嵌入,与非指针相比,我们不能直接调用嵌入的结构体的字段,因为它目前还是 nil,这个时候我们必须先将结构体初始化,然后再进行操作,下文的代码有演示。
type Student struct{
Name *Name
*People
int
string
score int
}
type Name string{
fist string
last string
}
type People struct {
name string
year string
addr string
}
func main(){
var s = new(Student)
s.Name = new(Name)
s.People = new(People)
s.Name.first = "小明"
s.Name.last = "小红"
s.name= "小明"
s.year = "2000"
s.addr = "北京"
s.int = 12
s.string = "12"
s.score = 100
fmt.Println(s)
}
通过这个演示我们发现,其实嵌入更像是一种语法糖,它就跟将结构体当作类型,前面的结构体字段变量跟这个类型保持一致这种操作是同样的作用,只不过直接嵌入可以将嵌入的字段当作自己的字段,省略了中间的变量罢了,它有下面的等于关系
type Student struct{
//People *People
// 等于
// *People
// People People
// 等于
// People
}
type People struct {
name string
year string
addr string
}
当一个结构体拥有了其它嵌入的时,也一起拥有了在他们身上的方法,不过,非嵌入的那种,还是需要带上中间的字段变量名称比如:
package main
type Student struct {
Name Name
*People
int
string
score int
}
type Name struct {
first string
last string
}
type People struct {
name string
year string
addr string
}
func (s *Name)S() {
println("stuend-s")
}
// 如果这里不是定义在指针上的方法,这段代码将报错
// 原因是 people嵌入的时候还是nil,如果这里不是指针上的方法,是值上的方法
// 底层是需要 *People (取值) 操作的,但是这时候是nil,所以就会报错。
func (s *People)SetS() {
println("people-setS")
}
func main() {
var s = new(Student)
// 这里注意,因为不是嵌入的操作,所以它必须带上中间的 Name
s.Name.S()
s.SetS()
}
空结构体
通常,当我们需要一个临时的变量时,我们可以会想到设置一个 bool 类型,因为我们潜意识中感觉一个 bool 类型是比较小的,但是一个空的变量才是最小的,下面让我们看一个例子,这个例子发生在使用 channel 传递信息这个场景。
func main() {
sig := make(chan bool)
go func(){
time.Sleep(time.Second)
sig <- true
}()
<- sig
fmt.Println("任务已经完成")
}
没错,这段代码中,使用一个 bool 类型的 channel 是没什么问题的,你甚至也可以使用 int string 都可以,因为只是传递一个信息,信息的内容不重要,但是当我们站在优化的角度来考虑,这里的 bool 就不完美了,我们改成空的结构体即可:
func main() {
sig := make(chan struct{})
go func(){
time.Sleep(time.Second)
sig <- struct{}{}
}()
<- sig
fmt.Println("任务已经完成")
}
需要注意的是,一个空的结构体,表示它类型的方式是 struct{}
,而使用这个空结构体的方式就是 struct{}{}
,前面的大括号是跟 struct 一起的整体表示空结构体,后面的大括号表示一个空结构体类型的结构体调用
直接嵌套还是作为字段
type Pool struct {
wg sync.WaitGroup
JobQueue chan Job
dispatcher *dispatcher
}
// or
type Pool struct {
sync.WaitGroup
JobQueue chan Job
dispatcher *dispatcher
}
结构体嵌套可能带来的问题:
名称冲突 如果 Pool 结构体中还定义了 Add/Done/Wait 方法,和嵌套的 WaitGroup 中的方法就会产生冲突。
不必要的方法 嵌套整个 WaitGroup 会让 Pool 结构体拥有 Add/Done/Wait 等方法,但 Pool 可能只需要 Wait 就够了。
使结构体臃肿 嵌套整个 WaitGroup 会让 Pool 结构体看起来很臃肿,包含许多其实用不到的方法。
继承关系不清晰 Pool 并不是 WaitGroup 的一种,嵌套整个 WaitGroup 让人可能误以为它是在继承 WaitGroup。
所以,作为一个字段,可以避免上述问题,又可以保持必要的功能。
总结一下,嵌套要慎用,只有当:
- 被嵌套的类型方法不会与现有方法冲突
- 被嵌套的所有方法都会被用到
- 两者之间有逻辑上的继承关系
时,才更适合使用嵌套 (组合) 的方式。
否则,使用字段的方式可以获得必要的功能,而不引入嵌套的潜在问题。