Go中的结构体(就至关于其它语言里的class):golang
struct 声明:json
type (标识符) struct { field1 type field2 type }
例子:markdown
type Student struct { Name string Age int Score int }
结构体中字段的访问,和其余语言同样,使用点:数据结构
package main import "fmt" type Student struct { Name string Age int Score int } func main() { var stu Student stu.Name = "Adam" stu.Age = 18 stu.Score = 90 fmt.Println(stu) fmt.Println(stu.Name) fmt.Println(stu.Age) fmt.Println(stu.Score) }
struct 定义的3种形式:ide
var stu Student var stu *Student = new (Student) var stu *Student = &Student{}
后两种返回的都是指向结构体的指针,因此须要再跟个等号分配内存空间。而且,有些场景应该是须要用指针的结构体会更加方便。
强调一下, struct 是值类型。这里要用new来建立值类型。不是make,make是用来建立 map 、slice、channel 的。
结构体的访问形式以下:函数
stu.Name (*stu).Name
用上面两种形式访问都是能够的,可是定义的时候返回指针的话,标准作法仍是应该用指针来访问的,不过Go作了处理,能够简化,直接用第一种就够了,可是要知道调用的本质。布局
结构体是值类型,里面全部的字段在内存里是连续的:优化
package main import "fmt" type Student struct { Name string Age int Score int } func main() { var stu Student stu.Name = "Adam" stu.Age = 18 stu.Score = 90 fmt.Println(stu) fmt.Printf("%p\n", &stu) fmt.Printf("%p\n", &stu.Name) fmt.Printf("%p\n", &stu.Age) fmt.Printf("%p\n", &stu.Score) } /* 执行结果 PS H:\Go\src\go_dev\day5\struct\attribute> go run main.go {Adam 18 90} 0xc04204a3a0 0xc04204a3a0 0xc04204a3b0 0xc04204a3b8 PS H:\Go\src\go_dev\day5\struct\attribute> */
package main import "fmt" type Student struct { Name string Age int Score int } func main(){ var stu1 Student stu1.Name = "Adam" stu1.Age = 16 stu1.Score = 90 var stu2 Student = Student{ Name: "Bob", Age: 15, Score: 85, } var stu3 *Student = &Student{ Name: "Cara", Age: 18, Score: 80, } fmt.Println(stu1) fmt.Println(stu2) fmt.Println(&stu2) fmt.Println(stu3) fmt.Println(*stu3) } /* 执行结果 PS H:\Go\src\go_dev\day5\struct\init> go run .\main.go {Adam 16 90} {Bob 15 85} &{Bob 15 85} &{Cara 18 80} {Cara 18 80} PS H:\Go\src\go_dev\day5\struct\init> */
也能够在大括号里按位置传参数进行初始化和定义:this
type Student struct { Name string Age int Score int } var s1 Student s1 = Student {"stu1", 18, 90} var s2 Student = Student{"stu2", 20, 80}
用结构体定义数据类型指针
每一个节点包含下一个节点的地址,这样就把全部的节点串起来了。一般把链表中的第一个节点叫作链表头。
type Link struct { Name string Next *Link }
下面有头插法和尾插法建立链表,还有遍历链表的方法:
package main import "fmt" type Student struct { Name string next *Student } // 遍历链表的方法 func trans(p *Student) { for p != nil { fmt.Println(*p) p = p.next } } // 头插法,从左边插入 // 每一个新加入的元素都插入到头部元素的后面,这样的好处是头部元素的地址不变 func CreateLinkListLeft(p *Student) { var head = p for i := 0; i < 10; i++ { p := Student{ Name: fmt.Sprintf("stuL%d", i), } p.next = head.next head.next = &p } } // 尾插法,从右边加入 func CreateLinkListRight(p *Student) { var tail = p for i := 0; i < 10; i++ { p := Student{ Name: fmt.Sprintf("stuR%d", i), } tail.next = &p tail = &p } } func main() { var headL Student fmt.Println("头插法") CreateLinkListLeft(&headL) // 结构体是值类型,要改变里面的值,就是传指针 trans(&headL) var headR Student fmt.Println("尾插法") CreateLinkListRight(&headR) trans(&headR) }
还有双链表,详细就不展开了:
type Link struct { Name string Next *Link Prev *Link }
每一个节点都有2个指针,分别用来指向左子树和右子树:
type binaryTree struct { Name string left *binaryTree right *binaryTree }
这里只给一个深度优先的遍历方法:
// 遍历二叉树,深度优先 func trans(root *Student) { if root == nil { return } // 前序遍历 fmt.Println(root) trans(root.left) trans(root.right) }
最后3句的相对位置,主要是打印的方法的位置不一样又有3种不一样的叫法。上面这个是前序遍历。若是打印放中间就是中序遍历。若是打印放最后,就是后序遍历。
广度优先的遍历方法,暂时能力还不够。另外若是要验证上面的遍历方法,也只能用笨办法来建立二叉树。
看下结构体里的一些高级用法
能够给结构体取别名:
type Student struct { Name string } type Stu Student // 取个别名
下面的代码,给原生的int类型取了个别名,也是能够像int同样使用的:
package main import "fmt" type integer int func main() { var i = integer = 100 fmt.Println(i) }
可是定义了别名的类型和原来的类型被系统认为不是同一个类型,不能直接赋值。可是是能够强转类型的:
type integer int func main() { var i integer = 100 var j int // j = i // 不一样的类型不能赋值 j = int(i) // 赋值须要强转类型 }
上面都是用原生的 int 类型演示的,自定义的结构体也是同样的。
golang 中的 struct 不像其余语言里的 class 有构造函数。struct 没有构造函数,通常可使用工厂模式来解决这个问题:
// go_dev/day5/struct/new/model/model.go package model // 名称是小写,就是不让你访问的 type student struct { Name string Age int } // 外部要调用的是这个工厂函数,返回上面的通过构造函数处理的完成了初始化的结构体,即实例 func NewStudent(name string, age int) *student { // 这里能够补充其余构造函数里的代码 return &student{ Name: name, Age: age, } } // go_dev/day5/struct/new/main/main.go package main import ( "../model" "fmt" ) func main() { s := model.NewStudent("Adam", 20) fmt.Println(*s) }
能够为 struct 中的每一个字段,写上一个tag。这个 tag 能够经过反射的机制获取到。
为字段加说明
type student struct { Name string "This is name field" Age int "This is age field" }
json序列化
最经常使用的场景就是 json 序列化和反序列化。先看一下序列化的用法:
package main import ( "fmt" "encoding/json" ) type Stu1 struct{ name string age int score int } type Stu2 struct { Name string Age int score int // 这个仍是小写,因此仍是会有问题 } func main() { var s1 Stu1 = Stu1 {"Adam", 16, 80} var s2 Stu2 = Stu2 {"Bob", 17, 90} var data []byte var err error data, err = json.Marshal(s1) if err != nil { fmt.Println("JSON err:", err) } else { fmt.Println(string(data)) // 类型是 []byte 转成 string 输出 } data, err = json.Marshal(s2) if err != nil { fmt.Println("JSON err:", err) } else { fmt.Println(string(data)) } } /* 执行结果 PS H:\Go\src\go_dev\day5\struct\json> go run main.go {} {"Name":"Bob","Age":17} PS H:\Go\src\go_dev\day5\struct\json> */
结构体中,小写的字段外部是访问不了的,因此第一个输出是空的。而第二个结构体中只有首字母大写的字段才作了序列化。
因此通常结构体里的字段名都是首字母大写的,这样外部才能访问到。不过这样的话,序列化以后的变量名也是首字母大写的。而json是能够实现跨语言传递数据的,可是在其余语言里,都是习惯变量小写的。这样go里json序列化出来的数据在别的语言里看就很奇怪。
在go的json包里,经过tag帮咱们作了优化。会去读取字段的tag,去里面找到json这个key,把对应的值,做为字段的别名。具体作法以下:
package main import ( "fmt" "encoding/json" ) type Student struct{ Name string `json:"name"` Age int `json:"age"` Score int `json:"score"` } func main() { var stu Student = Student{"Cara", 16, 95} data, err := json.Marshal(stu) if err != nil { fmt.Println("JSON err:", err) return } fmt.Println(string(data)) // 类型是 []byte 转成 string 输出 } /* 执行结果 PS H:\Go\src\go_dev\day5\struct\json_tag> go run main.go {"name":"Cara","age":16,"score":95} PS H:\Go\src\go_dev\day5\struct\json_tag> */
反引号,做用和双引号同样,不过内部不作转义。
结构体力的字段能够没有名字,即匿名字段。
type Car struct { Name string Age int } type Train struct { Car // 这个Car也是类型,上面定义的。这里没有名字 Start time.TIme int // 这个字段也没有名字,即匿名字段 }
访问匿名字段
能够直接经过匿名字段的类型来访问,因此匿名字段的类型不能重复:
var t Train t.Car.Name = "beemer" t.Car.Age = 3 t.int = 100
对于结构体类型,还能够在简化,结构体的名字能够不写,下面的赋值和上面的效果同样:
var t Train t.Name = "beemer" t.Age = 3
匿名字段冲突处理
type Car struct { Name string Age int } type Train struct { Car Start time.TIme Age int // 这个字段也叫 Age } var t Train t.Age // 这个Age是Train里的Age t.Car.Age // Car里的Age如今只能把类型名加上了
经过匿名字段实现继承
匿名字段在须要有继承的的场景下很好用:
type Animal struct { Color string Age int Weight int Type string } type Dog Struct { Animal Name string Weight float32 }
定义了一个 Animal 动物类,里面有不少属性。再定义一个 Dog 狗的类,也属于动物,须要继承动物的属性。这里用匿名字段就方便的继承过来了。而且有些字段还能够再从新定义覆盖原先的,好比例子里的 Weight 。这样 Dog 就有 Animal 的全部的字段,而且 Dog 还能添加本身的字段,也能够利用冲突覆盖父类里的字段。
Golang 中的方法是做用在特定类型的变量上的。所以自定义类型也能够有方法,而不只仅是 struct 。
func (变量名 方法所属的类型) 方法名 (参数列表) (返回值列表) {}
方法和函数的区别就是在func关键字后面多了 (变量名 方法所属的类型) 。这个也别称为方法的接收器(receiver)。这个是声明这个方法是属于哪一个类型,这里的类型也包括 struct。
Golang里的接收器没有 this 或者 self 这样的特殊关键字,因此名字能够任意取的。通常而言,出于对一致性和简短的须要,咱们使用类型的首字母。类比 self ,也就知道这个接收器的变量在方法定义的代码块里就是代指当前类型的实例。
package main import "fmt" type Student struct { Name string Age int } func (s *Student) growup () { s.Age++ } func (s *Student) rename (newName string) { s.Name = newName } func main() { var stu Student = Student{"Adam", 17} fmt.Println(stu) stu.growup() fmt.Println(stu) stu.rename("Bob") fmt.Println(stu) } /* 执行结果 PS H:\Go\src\go_dev\day5\method\beginning> go run main.go {Adam 17} {Adam 18} {Bob 18} PS H:\Go\src\go_dev\day5\method\beginning> */
上面的2个方法里的接收器类型加了星号。若是不用指针的话,传入的是对象的副本,方法改变是副本的值,不会改变原来的对象。
另外上面调用方法的用法也已经简写了,实际是经过结构体的地址调用的 (&stu).growup()
。
匿名字段就是继承的用法。不但能够继承字段,方法也是继承的:
package main import "fmt" type Animal struct { Type string } func (a Animal) hello() { fmt.Println(a.Type, "Woo~~") } type Dog struct { Animal Name string } func main() { var a1 Animal = Animal{"Tiger"} var d1 Dog d1.Type = "Labrador" d1.Name = "Seven" a1.hello() d1.hello() // Dog 也能调用 Animal 的方法 }
多继承
一个 struct 里用了多个匿名结构体,那么这个结构体就能够直接访问多个匿名结构体的方法,从而实现了多继承。
若是一个 struct 嵌套了另外一个匿名 struct,这个结果能够直接访问匿名结构的方法,从而实现了继承。
若是一个 struct 嵌套了另外一个有名 struct,这个模式就叫组合:
package main import "fmt" type School struct { Name string City string } type Class struct { s School Name string } func main() { var s1 School = School{"SHHS", "DC"} var c1 Class c1.s = s1 c1.Name = "Class One" fmt.Println(c1) fmt.Println(c1.s.Name) }
继承与组合的区别:
若是一个变量实现了 String() 方法,那么 fmt.Println 默认会调用这个变量的 String() 进行输出。
package main import "fmt" type Animal struct { Type string Weight int } type Dog struct { Animal Name string } func (d Dog) String () string{ return d.Name + ": " + d.Type } func main(){ var a1 Animal = Animal{"Tiger", 230} var d1 Dog = Dog{Animal{"Labrador", 100}, "Seven"} fmt.Println(a1) fmt.Println(d1) } /* 执行结果 PS H:\Go\src\go_dev\day5\method\string_method> go run main.go {Tiger 230} Seven: Labrador PS H:\Go\src\go_dev\day5\method\string_method> */
注意传值仍是传指针,例子里定义的时候没有星号,是传值的,打印的时候也是传值就有想过。打印的使用用 fmt.Println(&d1)
也是同样的。可是若是定义的时候用了星号,就是传指针,打印的时候就必须加上&把地址传进去才有效果。不然就是按照原生的方法打印出来。
这是go语言多态的实现方式
Interface 类型能够定义一组方法,可是这些不须要实现,而且 interface 不能包含任何变量。
这里讲接口只是起个头,下一篇继续讲接口
定义接口使用 interface 关键字。而后只须要再里面定义一个或者多个方法就好,不须要实现:
type 接口名 interface { 方法名1(参数列表) 方法名2(参数列表) [返回值] 方法名3(参数列表) [返回值] }
interface 类型默认是一个指针,默认值是空 nil :
type example interface{ Method() } var a example // 这里定义了一个接口,a就是一个指针 // 目前a没有赋值,a里没有任何实现,a是一个空指针 a.Method() // a仍是一个空指针,里面没有任何实现,这句会报错
上面定义了a以后,还缺乏一步,给指针a指向一个具体的实现。
Golang 中的接口,不须要显式的实现,只要一个变量,含有接口类型中的全部方法,那么这个变量就实现了这个接口。所以,golang 中没有 implement 类型的关键字
若是一个变量含有了多个 interface 类型的方法,那么这个变量就实现了多个接口。
下面是一个接口实现的示例:
package main import "fmt" // 定义一个接口 type AnimalInterface interface { Sleep() // 定义一个方法 GetAge() int // 再定义一个有返回值的方法 } // 定义一个类 type Animal struct { Type string Age int } // 接下来要实现接口里的方法 // 实现接口的一个方法 func (a Animal) Sleep() { fmt.Printf("%s need sleep\n", a.Type) } // 实现了接口的另外一个方法 func (a Animal) GetAge() int { return a.Age } // 又定义了一个类,是上面的子类 type Pet struct { Animal Name string } // 重构了一个方法 func (p Pet) sleep() { fmt.Printf("%s need sleed\n", p.Name) } // 有继承,因此Age方法会继承父类的 func main() { var a1 Animal = Animal{"Dog", 5} // 建立一个实例 var aif AnimalInterface // 建立一个接口 aif = a1 // 由于类里实现了接口的方法,因此能够赋值给接口 aif.Sleep() // 能够用接口调用 a1.Sleep() // 使用结构体调用也是同样的效果,这就是多态 var p1 Pet = Pet{Animal{"Labrador", 4}, "Seven"} aif = p1 aif.Sleep() fmt.Println(aif.GetAge()) }
一种事务的多种形态,均可以按照统一的接口进行操做。
多态,简单点说就是:"一个接口,多种实现"。好比 len(),你给len传字符串就返回字符串的长度,传切片就返回切片长度。
package main import "fmt" func main() { var s1 string = "abcdefg" fmt.Println(len(s1)) var l1 []int = []int{1, 2, 3, 4} fmt.Println(len(l1)) }
参照上面的,本身写的方法也能够接收接口做为参数,对不一样的类型对应多种实现:
package main import "fmt" type Msg interface { Print() } type Message struct { msg string } func (m Message) Print() { fmt.Println("Message:", m.msg) } type Information struct { msg string level int } func (i Information) Print() { fmt.Println("Information:", i.level, i.msg) } func interfaceUse(m Msg) { m.Print() } func main() { message := Message{"Hello"} // 定义一个结构体 information := Information{"Hi", 2} // 定义另一个类型的结构体 // 这里并不须要 var 接口,以及赋值 interfaceUse(message) // 参数不看类型了,而看你是否知足接口 interfaceUse(information) // 虽然这里的参数和上面不是同一个类型,可是这里对参数的要求是接口 }
这里并不须要显式的用 var 声明接口以及赋值。
golang 中并无明确的面向对象的说法,能够将 struct 类比做其它语言中的 class。
constructor 构造函数
经过结构体的工厂模式返回实例来实现
Encapsulation 封装
经过自动的大小写控制可见
Inheritance 继承
结构体嵌套匿名结构体
Composition 组合
结构体嵌套有名结构体
Polymorphism 多态
经过接口实现
实现一个图书管理系统,具备如下功能: