文章目录
一、结构体
1.1 如何定义结构体
type name struct{
filed1 type
filed2 type
...
}
1.2 如何创建结构体
1.2.1 使用 new
t := new(T),其中 t 为结构体指针变量
1.2.2 使用字面量
t := T{filed1:value,filed2:value,...}
1.2.3 声明
var t T
1.3 访问结构体中的成员
无论变量是结构体类型还是结构体类型指针,
都可以使用 "." 给结构体中字段赋值,"." 也可以获取结构体字段的值,
即 "结构体.字段名"
例如:
structObject.Name = "rose"
structObject.Age = 15
1.4 自定义包的结构体
结构体类型再定义它的包中必须是唯一的,它的完全类型命名是:
pack.struct 形式
1.5 结构体作为参数传输
1.5.1 形式参数传输
传递副本,操作副本,不影响原对象
1.5.2 指针参数传输
传递指针,操作原对象,影响源对象状态
1.6 带标签的结构体
type name struct{
field1 type "tag content"
field2 type "tag content"
...
}
附属于字段的标签,用于文档说明或者开发过程中的特殊标记
标签的值是一串字符串,标签的内容只有通过 refect(反射) 包获取
values := refect.ValueOf(interface{}) 获取变量值
types := refect.TypeOf(interface{}) 获取变量类型
counts := value.NumField() //获取结构体所有字段数量(私有+公有+匿名 总和)
tag := type.Field(index).Tag //获取字段对应的附属标签
1.7 匿名字段
字段没有显示名字,仅有类型,此时类型就是字段名
同样遵循: 小写 private,大写 public(私有/公有是针对包而言)
在一个结构体中,对于每种数据类型只能有一个匿名字段
1.8 内嵌结构体
GO 语言没有继承和多态,只有封装
继承是通过内嵌或者组合来实现
type admin struct{
user Man.Person
level string
}
1.9 结构体存放在数组或切片中
1.10 值语义和引用语义
二、方法
2.1 方法声明和调用
2.1.1 声明
func (receiver type) methodName(arg1 type,...)(output1 type,...){
// TO DO
}
方法名大写开头 public,小写开头 private (私有/公有是针对包而言)
2.1.2 调用
receiver.methodName(arg1 type,...)
如果 receiver 是一个指针,实例是值类型,receiver.methodName 和(&receiver).methodName 是等价的
如果 receiver 是一个值,实例是引用传递,receiver.methodName 和 (*receiver).methodName 也是等价的
2.1.3 值接收者 VS 指针接收者
要改变内容必须只用指针接收者
结构体过大也考虑使用指针接收者
一致性: 如有指针接收者,最好都是指针接收者
2.2 方法和函数的区别
是否有接收者,函数不能重载,
方法可以在接收者类型不同上允许重载。
重载:方法名相同,参数个数或类型不同
2.3 如何扩展任意类型的方法
2.3.1 定义别名
例如: 模拟 一个栈结构、队列结构
2.3.2 使用组合
type admin struct{
user Man.Person
level string
}
2.4 工厂模式
type File struct{
fb int
name string
}
//工厂方法
func NewFile(fb int,name string) *File{
if fb < 0 {
return nil
}
return &File{fb,name}
}
2.5 未导出字段
私有字段(字段小写开头,不能用点语法直接访问的),提供 getter 和 setter 方法
2.6 GO 是如何实现继承
将一个已存在类型作为另一个类型的字段即为内嵌,如果内嵌类型是匿名字段的话,内嵌类型的方法则会"晋升"成为外层类型的方法。此时,如外层类型有方法名和内嵌类型方法的方法名一致(返回值类型是否一致无所谓),则优先调用外层类型方法。
已存在类型称为 : 父类
外层类型称为 : 子类
三、二叉树(递归结构体)
3.1 什么是二叉树
3.2 二叉树的定义
3.3 二叉树遍历
3.3.1 前序遍历
3.3.2 中序遍历
3.3.3 后序遍历
一、结构体
GO 语言中没有类的概念,因此在 GO 语言中结构体有着更为重要的地位。一个自定义类型由一系列属性组成,每个属性都有自己的类型和值。结构体的目的就是把数据聚集在一起,以便能够更加快捷地操作这些数据,在外部看来就像是一个实体。结构体是值类型。
1.1 如何定义结构体
结构体定义的方式一般如下,组成结构体类型的数据称为 字段。每个字段名字唯一且都有类型。字段类型可以是任意类型interface{}
。字段名大写开头表示 public,小写开头表示 private。
type Person struct{
Name string
Age int
...
}
1.2 如何创建结构体
1.2.1 使用 new
一般在结构体定义之后,使用 t := new(T)
给该结构体变量分配内存,返回指向已分配内存的指针,变量 t 是一个指向 T 的实例的指针,结构体字段的初始值是所属类型的零值。
func main() {
jack := new(Person)
//Name = "",Age = 0
fmt.Printf("Name = %q,Age = %d\n",jack.Name,jack.Age)
}
1.2.2 使用字面量
初始化一个结构体也可以通过结构体字面量来实现。
func structLiteral() {
//注意 : 必须以字段在结构体定义时的顺序写
rose := Person{"rose",15}
//字段名加一个冒号,值的顺序可以不一致,可以初始化部分字段的值(跟数组一样)
jack := Person{Name: "jack",Age: 15}
tom := Person{Age: 22}
//Name = "jack",Age = 15
fmt.Printf("Name = %q,Age = %d\n",jack.Name,jack.Age)
//Name = "",Age = 22
fmt.Printf("Name = %q,Age = %d\n",tom.Name,tom.Age)
//Name = "rose",Age = 15
fmt.Printf("Name = %q,Age = %d\n",rose.Name,rose.Age)
}
1.2.3 声明
如果声明 var t T
,那么也会给 t
分配内存,并初始化零值,这个时候 t
是类型 T 。
//声明一个结构体
func interfacesturct() {
var jack Person
fmt.Printf("Name = %q,Age = %d\n",jack.Name,jack.Age)
}
func main() {
//Name = "",Age = 0
interfacesturct()
}
1.3 访问结构体中的成员
**在 GO 语言中,使用 点语法给字段赋值,同理,也可以使用点语法获取字段的值。**这个 " . " 在 GO 语言中叫做 选择器。使用 t := new(T)
创建的结构体,其实 t
是一个结构体指针,但是同样可以使用 点语法 直接获取结构体的 字段,不必要像 c++那样使用 ->
操作符,GO 语言会自动做转换。GO 语言的结构体与它的数据在内存中是以连续内存存在的,即便是嵌套结构体也是一样的。(这里也可以总结一点:使用new(T)
得到的变量是对应 T 类型的指针)
func visitStructField() {
jack := new(Person)
//结构体指针指针使用 . 语言直接给结构体变量赋值
jack.Name = "jack"
//也可将结构体指针转化成值类型再使用 . 语法给结构体变量赋值
(*jack).Age = 15
fmt.Printf("jack.Name = %s,jack.Age = %d\n",jack.Name,jack.Age)
}
1.4 自定义包的结构体
结构体类型再定义它的包中必须是唯一的,它的完全类型命名是:pack.struct 形式。
一个目录下只能有一个包,包名和目录名可以不一样。
后面的章节会讲到关于包的概念。
如果我们将 上面定义的 Peson 结构体放在 一个 Man 包中的话,那此时结构体的完整类型就变成了 “包名.结构体名”,即 Man.Person
package Man
type Person struct {
Name string
Age int
}
将创建结构体的代码改一改
func newStruct() {
jack := new(Man.Person)
fmt.Printf("Name = %q,Age = %d\n",jack.Name,jack.Age)
}
1.5 结构体作为参数传输
我们可以像其他数据类型一样将结构体类型作为参数传递给函数,访问结构体变量,传递结构体变量有形式参数传递和指针参数传递两种方式。
1.5.1 形式参数传输
传递副本,操作副本,不影响原对象
func operateSturct1(person Man.Person) {
person.Age = 22
}
1.5.2 指针参数传输
传递指针,操作原对象,影响原对象状态
func operateStruct2(person *Man.Person) {
person.Age = 26
}
调用
func structAsParams() {
jack := new(Man.Person)
jack.Name = "jack"
jack.Age = 15
//作为值类型传递,函数操作的是副本
operateSturct1(*jack)
fmt.Printf("jack.Age = %d\n",jack.Age)
//作为指针参数传递,操作原对象
operateStruct2(jack)
fmt.Printf("jack.Age = %d\n",jack.Age)
}
func main() {
structAsParams()
//jack.Age = 15
//jack.Age = 26
}
1.6 带标签的结构体
结构体中的字段不仅有名字和类型,还有一个可选的附属字段的标签: 用于文档说明或者开发过工程中的特殊标记,他的值是一串字符串。标签内容只有refect
(反射机制)包能获取。如下所示 :
姓名
是字段 Name
的附属标签(单引号支持跨行,不解析转义字符)
年龄
是字段Age
的附属标签(双引号仅支持一行,解析转义字符)
package Man
type Person struct {
Name string `姓名`
Age int "年龄"
}
下面演示如何获取标签内容
func tagFieldStruct() {
jack := new(Man.Person)
jack.Name = "jack"
jack.Age = 15
//获取 jack 的值
values := reflect.ValueOf(*jack)
// 获取 jack 的类型
types := reflect.TypeOf(*jack)
// 获取字段数量(私有+共有+匿名变量总和)
counts := values.NumField()
for index := 0;index < counts;index++ {
//获取每一个字段
objectField := types.Field(index)
//获取附属于当前字段的标签
tag := objectField.Tag
fmt.Println(tag)
//姓名
//年龄
}
}
1.7 匿名字段
结构体可以包含一个或多个匿名字段,但是每一种数据类型只能有一个匿名字段。匿名字段即字段没有显示的名字,只有字段对应的数据类型,此时类型就是字段的名字。此时,也遵循小写开头 private, 大写开头为 public。
type Goods struct {
Name string "商品名字"
price float64 "商品价格"
int "商品编号"
}
//匿名字段
func anonymousField() {
// goods 为结构体指针变量
goods := new(Goods)
goods.Name = "鲱鱼罐头"
goods.price = 89.5
goods.int = 20200456
fmt.Println(*goods)
}
1.8 内嵌结构体
数组从某种意义上来说也算是结构体体,index 相当于 字段。
GO 语言没有继承和多态,只有封装。
结构体也是一种数据类型,所以它同样可以作为匿名字段使用。结构体可以被内嵌(或组合)入结构体当成一个字段使用,当内嵌结构体的时候,访问字段仍然使用 点语法。模拟继承。
//组合结构体
type admin struct {
user Man.Person
level string
}
//继承
func inheritance() {
// 1.作为值类型
var tom admin
tom.user.Name = "tom"
tom.level = "PM"
fmt.Println(tom) //{{tom 0} PM}
// 2.作为字面量
jack := admin{
//注意: 跨行初始化需要用 ","结尾,单行不需要
user: Man.Person{
Name: "jack",
Age: 25,
},
//user: Man.Person{Name: "jack", Age: 25},
level: "CTO",
}
fmt.Println(jack) //{{jack 25} CTO}
// 3.作为指针
rose := new(admin)
rose.user.Name = "rose"
rose.user.Age = 25
rose.level = "CEO"
fmt.Println(*rose) //{{rose 25} CEO}
}
1.9 结构体存放在数组或切片中
func structslices() {
// 数组(可以省略结构体前面的 Man.Person)
arrays := []Man.Person{
{
Name: "jack",
Age: 15,
},
{
Name: "rose",
Age: 23,
},
}
//arrays = [{jack 15} {rose 23}]
fmt.Printf("arrays = %v\n",arrays)
// 切片
slice := make([]Man.Person,0)
slice = append(slice, Man.Person{Name: "jack",Age: 23})
//slice = [{jack 23}]
fmt.Printf("slice = %v\n",slice)
}
1.10 值语义和引用语义
值语义和引用语义区别在于赋值。
b = a
b.Modify()
如果 b 的修改不会影响 a 的值,那么此类型属于值类型,也就意味着: b 会完全拷贝 a,成为 a 的一个副本。
反之,就是引用类型。
数组、切片、结构体 都是值类型。
GO 语言中类型的值语义表现非常彻底,这也就意味着在调用标准库的时候,更多的是传入值语义类型的参数(这句话仅供参考)。
引用传递,传递的是一个指针。指针的值被复制,但指针的值指向的地址上的那个值不会被复制(被复制的是指针,但是两个指针指向同一个实际的值),如果修改这个指针的值,实际上意味着这个值所指向的地址上的值被修改。(其实,指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址,所以说到底还是值传递)
二、方法
在 GO 语言中,方法和函数不是同一个东西,有明确的区分。方法有接收者,函数没有接收者。接收者如何理解呢,也很简单,有语言基础的应该能明白。我们日常开发中,例如 定义 人,人有姓名,年龄。这个属于属性,说话聊天这个属于动作。如果用 class 来定义的话,写下 Java 代码:
class Person{
//姓名
public String name;
//年龄
public int age;
//说话
public void speak() {
System.out.println("说话");
}
}
//main 方法
public class Test {
public static void main(String[] args) {
Person jack = new Person();
jack.name = "jack";
jack.age = 23;
jack.speak();
}
}
如上: jack 就是方法接收者,相当于给 jack 发送 speak() 消息, 只有 Person 类型的实例对象才具备调用资格。
类型 + 类型方法 = 面向对象中的一个类。
在 GO 语言中,类型的代码和他相关的方法代码可以分布在同一个包下的不同源文件。方法不允许重载。
2.1 方法声明和调用
2.1.1 方法声明
方法的定义如下格式,在 方法名之前, func 关键字 之后的括号中指定接收者。接收者几乎可以是任意类型,包括结构体类型、函数类型、指针类型、int、string、数组或别名,但不能是接口类型,因为接口是一个抽象定义。
func (receiver type) methodName(arg1 type,...)(ouputValue1 type,...){
//TO DO
}
2.1.2 方法调用
如果 receiver
是接收者的一个实例,methodName
是接收者类型的一个方法名,那么方法的调用遵循传统的选择器符号receiver.methodName()
。
如果 receiver 是一个指针,实例是值类型,receiver.methodName 和(&receiver).methodName 是等价的
如果 receiver 是一个值,实例是引用传递,receiver.methodName 和 (*receiver).methodName 也是等价的
结构体定义如下:
package Man
import "fmt"
type Person struct {
Name string `姓名`
Age int "年龄"
}
func (receiver *Person) Speak(){
receiver.Age = 28
fmt.Printf("%s 说话\n",receiver.Name)
}
func (receiver Person) Eat() {
receiver.Age = 28
fmt.Printf("%s 吃饭 \n",receiver.Name)
}
调用:
func callStructmethod() {
//指针类型
jack := new(Man.Person)
jack.Name = "jack"
jack.Age = 15
jack.Speak()
fmt.Println(*jack)
// 值类型
rose := Man.Person{
Name: "rose",
Age: 15,
}
rose.Eat()
fmt.Println(rose)
}
输出结果:
jack 说话
{jack 28}
rose 吃饭
{rose 15}
2.1.3 值接收者 VS 指针接收者
从上面的例子打印结果来看,分别在 指针接收者的 Speak()
中修改接收者的值,结果值给修改。而 值接收者 Eat()
也修改了接收者的值,但是原实例的值却未得到修改,说明操作的是其副本。综上,我们也可得到如下结论:
要改变内容必须只用指针接收者
结构体过大也考虑使用指针接收者
一致性: 如有指针接收者,最好都是指针接收者
2.2 方法和函数的区别
其实,就是声明上格式的区别而已。
函数不能重载。
方法对于同一类型只能有一个给定名称的方法,但是如果是基于接收者类型,则允许重载,也就是说具有同样名字的方法可以在多个不同的接收者类型上存在。
重载:方法名相同,参数个数或参数类型不同
2.3 如何扩展任意类型的方法
GO 语言中的大多数类型都是值语义,并且都可以包含对应的操作方法。在需要的时候,你可以给任意类型"增加"新方法。GO 开发中,一般有以下两种方式为已有类型或自定义类型添加新的方法: 定义别名 和 组合 。
2.3.1 定义别名
队列 :先进先出
type Queue []interface{}
//加入队列
func (q *Queue) Push(anyObject interface{}) (status bool){
if anyObject != nil{
*q = append(*q,anyObject)
return true
}
return false
}
//踢出队列
func (q *Queue) Pop()(object interface{}) {
if len(*q) > 0 {
header := (*q)[0]
*q = (*q)[1:]
return header
}
return nil
}
//判断队列是否为空
func (q *Queue) IsEmpty() (isEmpty bool) {
return len(*q) == 0
}
//队列长度
func (q *Queue) Size() (size int) {
return len(*q)
}
//main 方法
func main() {
queues := make(Queue,0)
queues.Push(1)
queues.Push(2)
queues.Push(3)
fmt.Printf("queue size = %d\n",queues.Size())
fmt.Printf("queue is Empty = %t\n",queues.IsEmpty())
fmt.Printf("queue object is = %v\n",queues.Pop())
fmt.Printf("queue object is = %v\n",queues.Pop())
fmt.Printf("queue object is = %v\n",queues.Pop())
fmt.Printf("queue is Empty = %t\n",queues.IsEmpty())
/* 输出结果:
queue size = 3
queue is Empty = false
queue object is = 1
queue object is = 2
queue object is = 3
queue is Empty = true
*/
}
栈结构 : 后进先出
/*
定义类型别名,模拟栈结构: 先进后出
*/
type Stack []interface{}
//入栈
func (stack *Stack) Push(anyObject interface{})(status bool) {
if anyObject != nil{
*stack = append(*stack,anyObject)
return true
}
return false
}
//出栈
func (stack *Stack) Pop()(object interface{}) {
if !stack.IsEmpty() {
value := (*stack)[len(*stack)-1]
*stack = (*stack)[:len(*stack)-1]
return value
}
return nil
}
//判断是否为空
func (stack *Stack) IsEmpty() bool {
return len(*stack) == 0
}
//栈中对象数量
func (stack *Stack) Size() int {
return len(*stack)
}
//main方法
func main() {
stack := make(Stack,0)
stack.Push(1)
stack.Push(2)
stack.Push(3)
fmt.Printf("stack size = %d\n",stack.Size())
fmt.Printf("stack po object = %d\n",stack.Pop())
fmt.Printf("stack po object = %d\n",stack.Pop())
fmt.Printf("stack po object = %d\n",stack.Pop())
fmt.Printf("stack is empty = %t\n",stack.IsEmpty())
}
/*输出结果:
stack size = 3
stack po object = 3
stack po object = 2
stack po object = 1
stack is empty = true
*/
2.3.2 使用组合
使用内嵌结构体来实现
type admin struct {
user Man.Person
level string
}
2.4 工厂模式
在面向对象编程中,通过使用构造方法(GO 中没有构造方法) 实现工厂模式。在 GO 中,也提供了类似的解决方案。以结构体为例
type File struct{
fb int "文件描述符"
name string "文件名字"
}
下面是这个结构体类型对应的工厂方法,它返回一个结构体实例的指针
func NewFile(fb int,name string) *File{
if fb < 0 {
return nil
}
return &File{fb,name}
}
2.5 未导出字段
当类型被明确导出时,假设存在私有字段(小写开头)它的字段并没有全部被导出,想在另一个程序中访问未导出字段可以通过提供 getter 和 setter 来完成。
type Person struct{
firstName string
lastName string
}
// setter
func (p *Peerson)SetterFirstName(newName string){
p.firstName = newName
}
// getter
func (p *Person)GetterFirstName() string{
return p.firstName
}
2.6 GO 是如何实现继承
将一个已存在类型作为另一个类型的字段即为内嵌,如果内嵌类型是匿名字段的话,内嵌类型的方法则会"晋升"成为外层类型的方法。此时,如外层类型有方法名和内嵌类型方法的方法名一致(返回值类型是否一致无所谓),则优先调用外层类型方法。
前置条件 : 内嵌必须是匿名字段 如果不是匿名字段的话,必须通过外层类型先行访问该字段,再继续点语法方法该字段的方法。
2.6.1 匿名字段
type Point struct {
x,y float64
Name string
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x+p.y*p.y)
}
type NamePoint struct {
Point //此处是匿名字段
}
func main() {
object := NamePoint{
Point:Point{
x :3,
y: 4,
Name:"go"
},
}
//因为是匿名字段,所有可以直接调用 内嵌类型的方法和字段
fmt.Println(object.Abs(),object.Name)
//5 cherish
}
非匿名字段情况: 遵循一般访问规则
type Point struct {
x,y float64
Name string
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x+p.y*p.y)
}
type NamePoint struct {
Field Point //显式字段
}
func main() {
object := NamePoint{
Field:Point{
x :3,
y: 4,
Name:"hhaa",
},
}
//遵循 点语法规则
fmt.Println(object.Field.Abs(),object.Field.Name)
}
2.6.1 作为匿名字段 且 外层类型定义了内嵌类型一样的方法名
type Point struct {
x,y float64
Name string
}
func (p *Point) Abs() float64 {
return math.Sqrt(p.x*p.x+p.y*p.y)
}
type NamePoint struct {
Point //匿名字段
city string
}
//如果 NamePoint 定义了和内嵌类型 Point 同名方法(方法名一样,返回值类型可一样,可不一样),则优先调用外层结构体的方法
// 则可以理解为 NamePoint 继承了 Point ,并覆写了 Abs() 方法
// 根据面向对象的思路: 优先调用子类的方法
func (n *NamePoint) Abs() float64 {
//注意:这里如果写 n.Abs() 就是递归调用了
return n.Point.Abs() * 10
}
func main() {
object := NamePoint{
Point:Point{
x :3,
y: 4,
Name: "go",
},
city: "china",
}
//拥有访问匿名结构体的方法和字段的资格
fmt.Println(object.Abs(),object.Name,object.city)
//50 go china
}
三、二叉树(递归结构体)
3.1 什么是二叉树
二叉树中每个节点最多能链接两个节点: 左节点(leftTree
) 和 右节点(rightTree
),这两个节点本身又可以有左右节点,以此类推。树的顶层节点 叫做根节点(root
),底层没有子节点的节点叫做叶子节点(leaves
),叶子节点的 leftTree
和 rightTree
指针值为 nil
值。
3.2 二叉树的定义
在 GO 语言中,可以用结构体定义二叉树的结构:
type TreeNode struct {
leftTree *TreeNode "左节点"
value string "节点"
rightTree *TreeNode "右节点"
}
3.3 二叉树遍历
二叉树的遍历有前中后序遍历 3 种方式。所谓的前中后序遍历,指的是 节点的位置 分别在 前中后。即: 根在最前面、中间、最后面。左右遵循先左后右原则。 了解更多
//定义一个全局切片,装载遍历结果
var VisitSlice = make([]string,0)
//提供访问节点的value,并加入切片
func (node TreeNode) visitValue() {
VisitSlice = append(VisitSlice,node.value)
}
func createTree() *TreeNode {
tree := new(TreeNode)
//根节点
tree.value = "A"
//左
tree.leftTree = &TreeNode{value:"B"}
tree.leftTree.leftTree = &TreeNode{leftTree: nil,value: "D",rightTree: nil}
tree.leftTree.rightTree = &TreeNode{leftTree: nil,value: "E",rightTree: nil}
//右
tree.rightTree = &TreeNode{value: "C"}
tree.rightTree.leftTree = &TreeNode{leftTree: nil,value: "F",rightTree: nil}
tree.rightTree.rightTree = &TreeNode{leftTree: nil,value: "G",rightTree: nil}
return tree
}
3.3.1 前序遍历
//前序遍历
func (node *TreeNode)firstVisit() {
if node == nil{
return
}
node.visitValue()
node.leftTree.firstVisit()
node.rightTree.firstVisit()
}
func main(){
tree := createTree()
tree.firstVisit()
// 前序遍历:[A B D E C F G]
fmt.Printf("前序遍历:%v\n",VisitSlice)
}
3.3.2 中序遍历
//中序遍历
func (node *TreeNode) middleVisit() {
if node == nil{
return
}
node.leftTree.middleVisit()
node.visitValue()
node.rightTree.middleVisit()
}
func main(){
tree := createTree()
tree.middleVisit()
//中序遍历:[D B E A F C G]
fmt.Printf("中序遍历:%v\n",VisitSlice)
}
3.3.3 后序遍历
//后序遍历
func (node *TreeNode) lastVisit() {
if node == nil{
return
}
node.leftTree.lastVisit()
node.rightTree.lastVisit()
node.visitValue()
}
func main(){
tree := createTree()
tree.lastVisit()
//后序遍历:[D E B F G C A]
fmt.Printf("后序遍历:%v\n",VisitSlice)
}
有问题请留言,谢谢。