普通玩家选择标准配置,高端玩家选择自定义配置。
一、struct简介
go语言中没有像类的概念,但是可以通过结构体struct实现oop(面向对象编程)。struct的成员(也叫属性或字段)可以是任何类型,如普通类型、复合类型、函数、map、interface、struct等,所以我们可以理解为go语言中的“类”。
二、struct详解
1、struct定义
在定义struct成员时候区分大小写,若首字母大写则该成员为公有成员(对外可见),否则是私有成员(对外不可见)。
struct {
字段1 类型
字段2 字段3 类型
...
}
自定义一个结构体类型
type struct_variable_type struct {
member member_type
member member_type
.....
member member_type
// 示例
type student struct{
name string
sex string
id int
score int
}
声明与初始化
var stu1 Student
var stu2 *Student = &Student{} //简写stu2 := &Student{}
var stu3 *Student = new(Student) // 简写stu3 := new(Student)
2、struct使用
在struct中,无论使用的是指针的方式声明还是普通方式,访问其成员都使用“.”,在访问的时候编译器会自动把stu2.name转为(*stu2).name。
struct分配内存使用new,返回的是指针。
struct没有构造函数,但是可以自定义“构造函数”。
struct是自定义的类型,不能和其他类型进行强制转换。
package main
import "fmt"
type Student struct{
name string
age int
Class string
}
func main(){
var stu1 Student
stu1.age = 22
stu1.name = "wpr"
stu1.Class = "Class1"
fmt.Println(stu1.name,stu1.age,stu1.Class) // wpr 22 Class1
var stu2 *Student = new(Student)
stu2.name = "wpr"
stu2.age = 33
fmt.Println(stu2.name,(*stu2).age) // wpr 33
var stu3 *Student = &Student{name:"wpp",age:18,Class:"class3"}
fmt.Println(stu3.name,(*stu3).Class) // wpp class3
}
3、自定义构造函数
以下是通过工厂模式自定义构造函数方法(工厂模式:不管丢进去什么,都产生一个对象)
package main
import "fmt"
type Student struct{
name string
age int
Class string
}
func Newstu(name1 string,age1 int,class1 string) *Student {
return &Student{name:name1,age:age1,Class:class1}
}
func main() {
stu1 := Newstu("wpr",22,"math")
fmt.Println(stu1.name) // wpr
}
4、tag
tag可以为结构体的成员添加说明或者标签便于使用,这些说明可以通过反射获取到。
结构体中的成员首字母小写对外不可见,但是我们把成员定义为首字母大写,这样与外界进行数据交互会带来极大的不便,此时tag带来了解决方法。
type Student struct {
Name string "the name of student"
Age int "the age of student"
Class string "the class of student"
}
应用场景,json序列化操作:
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `jason:"name"`
Age int `json:"age"`
}
func main() {
var stu = Student{Name:"wpr",Age:22}
data,err := json.Marshal(stu) // json.Marshal是将数据编码成json字符串
if err != nil{
fmt.Println("json encode failed err:",err)
return
}
fmt.Println(string(data)) // {"Name":"wpr","age":22}
}
5、匿名成员(字段、属性)
结构体中,每个成员不一定都有名称,也允许字段没有名字,即匿名成员。
匿名成员的一个重要作用,可以用来实现oop中的继承。
同一种类型匿名成员只允许最多存在一个。
当匿名成员是结构体时,且两个结构体中都存在相同字段时,优先选择最近的字段。
package main
import "fmt"
type Person struct {
Name string
Age int
}
type Student struct{
score string
Age int
Person
}
func main() {
var stu = new(Student)
stu.Age = 22 // 优先选择Student中的Age
fmt.Println(stu.Person.Age,stu.Age) // 0 22
}
6、继承、多继承
当结构体中的成员也是结构体时,该结构体就继承了这个结构体,继承了其所有的方法与属性,当然有多个结构体成员也就是多继承。
访问父结构中属性也使用“.”,但是当子结构体中存在和父结构中的字段相同时候,只能使用:“子结构体.父结构体.字段”访问父结构体中的属性,如上面示例的stu.Person.Age。
继承结构体可以使用别名,访问的时候通过别名访问,如下面示例man1.job.Salary:
package main
import "fmt"
type Person struct{
Name string
Age int
}
type Teacher struct{
Salary int
Classes string
}
type man struct{
sex string
job Teacher // 别名,继承Teacher
Person // 继承Person
}
func main() {
var man1 = new(man)
man1.Age = 22
man1.Name = "wpr"
man1.job.Salary = 8500
fmt.Println(man1,man1.job.Salary) // &{ {8500 } {wpr 22}} 8500
}
7、结构体中的方法
go语言中的方法是作用在特定类型的变量上,因此自定义的类型都可以有方法,不仅仅是在结构体中。
go中的方法和传统的类的方法不太一样,方法和类并非组织在一起,传统的oop方法和类放在一个文件里面,而go语言只要在同一个包里就可,可分散在不同文件里。go的理念就是数据和实现分离,引用官方说法:“Methods are not mixed with the data definition (the structs): they are orthogonal to types; representation(data) and behavior (methods) are independent”。
方法的调用通过recv.methodName(),其访问控制也是通过大小写区分。
方法定义,其中recv代表方法作用的结构体:
func (recv type) methodName(parameter_list) (return_value_list) {...}
package main
import "fmt"
type Person struct{
Name string
Age int
}
func (p Person) Getname() string { //p代表结构体本身的实例,类似python中的self,这里p可以写为self
fmt.Println(p.Name)
return p.Name
}
func main() {
var person1 = new(Person)
person1.Age = 22
person1.Name = "wpr"
person1.Getname()
}
// 结果:wpr
当有了结构的方法时候,我们可以自定义其初始化方法,由于结构体是值类型,所以我们使用指针才能改变其存储的值。
package main
import "fmt"
type Person struct{
Name string
Age int
}
func (self *Person) init(name string,age int){
self.Name = name
self.Age = age
}
func main(){
var person1 = new(Person)
person1.init("wpr",23)
// (&person1).init("wpr",23)
fmt.Println(person1) //&{wpr 23}
}
如果实现了结构体中的String方法,在使用fmt打印时候会调用该方法,类似与python中的_ str _方法。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (self *Person) String() string {
return self.Name
}
func main() {
var person1 = new(Person)
person1.Name = "wpr"
person1.Age = 22
fmt.Println(person1) // wpr
}
8、内存分布
go中的结构体内存布局和c结构体布局类似,每个成员的内存分布是连续的,在以下示例中通过反射进行进一步说明:
package main
import (
"fmt"
"reflect"
)
type Student struct {
Name string
Age int64
wight int64
high int64
score int64
}
func main() {
var stu1 = new(Student)
fmt.Printf("%p\n", &stu1.Name)
fmt.Printf("%p\n", &stu1.Age)
fmt.Printf("%p\n", &stu1.wight)
fmt.Printf("%p\n", &stu1.high)
fmt.Printf("%p\n", &stu1.score)
typ := reflect.TypeOf(Student{})
fmt.Printf("Struct is %d bytes long\n", typ.Size())
n := typ.NumField()
for i := 0; i < n; i++ {
field := typ.Field(i)
fmt.Printf("%s at offset %v,size=%d,align=%d\n", field.Name, field.Offset, field.Type.Size(), field.Type.Align())
}
}
/* 结果
0xc00008e000
0xc00008e010
0xc00008e018
0xc00008e020
0xc00008e028
Struct is 48 bytes long
Name at offset 0,size=16,align=8
Age at offset 16,size=8,align=8
wight at offset 24,size=8,align=8
high at offset 32,size=8,align=8
score at offset 40,size=8,align=8
*/
在以上结果中,可以看到内存地址的偏移总是以8字节偏移(使用的是int64,刚好是8字节),再观察其内存地址,也是连续的,所以go语言中的结构体内存布局是连续的,如下图:
三、使用struct实现链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
链表有很多种不同的类型:单向链表,双向链表以及循环链表。
单链表
单链表:每个结点包含下一个结点的地址,这样把所有结点都串起来的链式数据结构叫做链表通常把链表中的第一个结点叫做表头。
为了方便,数据区域这里使用int
type Node struct {
data int
next *node
}
链表遍历
链表的遍历是通过移动指针进行遍历,当指针移动到最后一个结点时,其next指针为nil。
package main
import "fmt"
type Node struct {
data int
next *Node
}
// 遍历
func Shownode(p *Node) {
for p != nil {
fmt.Println(*p)
p = p.next // 移动指针
}
}
func main() {
var head = new(Node)
head.data = 1
var node1 = new(Node)
node1.data = 2
head.next = node1
var node2 = new(Node)
node2.data = 3
node1.next = node2
Shownode(head)
}
/* 结果
{1 0xc000082030}
{2 0xc000082040}
{3 <nil>}
*/
插入结点
单链表的结点插入方法一般使用头插法或者尾插法。
头插法:每次插入在链表的头部插入结点。
package main
import "fmt"
type Node struct {
data int
next *Node
}
func Shownode(p *Node) { // 遍历
for p != nil {
fmt.Println(*p)
p = p.next // 移动指针
}
}
func main() {
var head = new(Node)
head.data = 0
var tail *Node
tail = head // tail用于记录头结点的地址,刚开始tail的指针指向头结点
for i := 1; i < 10; i++ {
var node = Node{data: i}
node.next = tail // 将新插入的node的next指向头结点
tail = &node // 重新赋值头结点
}
Shownode(tail) // 遍历结果
}
/*
{9 0xc0000880b0}
{8 0xc0000880a0}
{7 0xc000088090}
{6 0xc000088080}
{5 0xc000088070}
{4 0xc000088060}
{3 0xc000088050}
{2 0xc000088040}
{1 0xc000088030}
{0 <nil>}
*/
尾插法:每次插入结点在尾部
package main
import "fmt"
type Node struct {
data int
next *Node
}
func Shownode(p *Node) { // 遍历
for p != nil {
fmt.Println(*p)
p = p.next // 移动指针
}
}
func main() {
var head = new(Node)
head.data = 0
var tail *Node
tail = head // tail用于记录最末尾的结点的地址,刚开始tail的指针指向头结点
for i := 1; i < 10; i++ {
var node = Node{data: i}
(*tail).next = &node
tail = &node
}
Shownode(head) // 遍历结果
}
/* 结果
{0 0xc000010050}
{1 0xc000010060}
{2 0xc000010070}
{3 0xc000010080}
{4 0xc000010090}
{5 0xc0000100a0}
{6 0xc0000100b0}
{7 0xc0000100c0}
{8 0xc0000100d0}
{9 <nil>}
*/