5 map、结构体、方法、面向对象【Go语言教程】
1 map
1.1 概念及声明
①概念
基本语法:var map 变量名 map[keytype]valuetype
- key 可以是什么类型?
golang 中的 map,的 key 可以是很多种类型,比如 bool, 数字,string, 指针, channel , 还可以是只包含前面几个类型的 接口, 结构体, 数组
通常 key 为 int 、string
注意: slice, map 还有 function 不可以,因为这几个没法用 == 来判断- valuetype 可以是什么类型?
valuetype 的类型和 key 基本一样,这里我就不再赘述了通常为: 数字(整数,浮点数),string,map,struct
②声明方式
- var a map[string]string
- var a map[string]int
- var a map[int]string
- var a map[string]map[string]string
注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用
func main(){
//map的声明和注意事项
var a map[string]string
//在使用map之前,需要先make(给map分配内存空间)
a = make(map[string]string, 10)
a["no1"] = "curry"
a["no2"] = "徐杰"
a["no3"] = "John"
a["no4"] = "欧文"
a["no3"] = "Bob"
fmt.Println(a)
}
对上面的代码进行说明:
- map 在使用前一定要 make
- map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
- map 的 value 是可以相同的.
- map 的 key-value 是无序
- make 内置函数数目
map的三种初始化方式:
package main
import (
"fmt"
)
func main(){
//map的初始化方式
//1. 先声明后make
var a map[string]string
//在使用map前,需要先make(给map分配数据空间)
a = make(map[string]string, 10)
a["no1"] = "天暗星"
a["no2"] = "天捷星"
a["no3"] = "天速星"
fmt.Println(a)
//2. 声明时直接make
cities := make(map[string]string)
cities["no1"] = "newYork"
cities["no2"] = "taibei"
cities["no3"] = "beijing"
fmt.Println(cities)
//3. 声明时,直接初始化并赋值
heros := map[string]string{
"hero1": "宋江",
"hero2": "赵云",
"hero3": "岳飞",
}
heros["hero3"] = "林冲"
fmt.Println("heros=", heros)
// map[no1:天暗星 no2:天捷星 no3:天速星]
// map[no1:newYork no2:taibei no3:beijing]
// heros= map[hero1:宋江 hero2:赵云 hero3:林冲]
}
1.2 map的crud
①map的增加和更新
map[“key”] = value //如果 key 还没有,就是增加,如果 key 存在就是修改。
func main(){
a := make(map[string]string)
//map中没有存在key1,因此直接新增
a["key1"] = "jack"
//map中已经存在了key1,因此直接更新
a["key1"] = "jackson"
//map[key1:jackson]
fmt.Println(a)
}
②map的删除
delete(map,“key”) ,delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在, 不操作,但是也不会报错
func main(){
a := make(map[string]string)
a["key1"] = "jack"
//删除key1
delete(a, "key1")
//map[]
fmt.Println(a)
}
注意:
如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除或者 map = make(…),make 一个新的,让原来的成为垃圾,被 gc 回收
③map的查找
对上面代码的说明:
说明:如果 heroes 这个 map 中存在 “no1” , 那么 findRes 就会返回 true,否则返回 false
func main(){
a := make(map[string]string)
a["key1"] = "jack"
a["key2"] = "tom"
val, ok := a["key3"]
if ok {
fmt.Println("值为:", val)
}else {
fmt.Println("没找到")
}
}
1.3 map的遍历与切片
①map的遍历
- 案例演示相对复杂的 map 遍历:该 map 的 value 又是一个 map
说明:map 的遍历使用 for-range 的结构遍历
package main
import (
"fmt"
)
func main(){
//map通过for-range方式来进行遍历
//1. 遍历普通map
cities := make(map[string]string)
cities["no1"] = "北京"
cities["no2"] = "天津"
cities["no3"] = "上海"
for k, v := range cities {
fmt.Printf("k=%v v=%v\n", k, v)
}
//2. 遍历双重map【map的value又是一个map】
studentMap := make(map[string]map[string]string)
//map使用之前需要make make(map[string]string, 3)
studentMap["stu01"] = make(map[string]string, 3)
studentMap["stu01"]["name"] = "jack"
studentMap["stu01"]["sex"] = "男"
studentMap["stu01"]["address"] = "北京胡同"
studentMap["stu02"] = make(map[string]string)
studentMap["stu02"]["name"] = "lucy"
studentMap["stu02"]["sex"] = "女"
studentMap["stu02"]["address"] = "上海外滩"
for k1, v1 := range studentMap {
fmt.Println("k1=", k1)
for k2, v2 := range v1 {
fmt.Printf("\t k2=%v v2=%v\n", k2, v2)
}
fmt.Println()
}
}
②map切片
切片的数据类型如果是 map,则我们称为 slice of map,map 切片,这样使用则 map 个数就可以动态变化了。
案例:
要求:使用一个 map 来记录 monster 的信息 name 和 age, 也就是说一个 monster 对应一个 map,并且妖怪的个数可以动态的增加=>map 切片
package main
import (
"fmt"
)
func main(){
//1. 声明一个map切片
var monsters []map[string]string
monsters = make([]map[string]string, 2) //准备放入2个妖怪
monsters[0] = make(map[string]string, 2)
monsters[0]["name"] = "牛魔王"
monsters[0]["age"] = "500"
monsters[1] = make(map[string]string, 2)
monsters[1]["name"] = "狐狸精"
monsters[1]["age"] = "300"
//下面写法越界,因为只开辟了两块空间
//panic: runtime error: index out of range [2] with length 2
// monsters[2] = make(map[string]string, 2)
//这里我们可以使用切片的append函数,动态的增加monster
//①定义monster信息
newMonster := map[string]string{
"name" : "新妖怪~火云邪神",
"age" : "200",
}
monsters = append(monsters, newMonster)
//[map[age:500 name:牛魔王] map[age:300 name:狐狸精] map[age:200 name:新妖怪~火云邪神]]
fmt.Println(monsters)
}
1.3 map的排序及使用细节
①map的排序
- golang 中没有一个专门的方法针对 map 的 key 进行排序
- golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样.
- golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可
package main
import (
"fmt"
"sort"
)
func main(){
//map的排序
map1 := make(map[int]int, 10)
map1[10] = 100
map1[1] = 13
map1[4] = 56
map1[0] = 90
fmt.Println(map1)
//map的排序:
//1. 先将map的key放入到切片中
//2. 对切片进行排序
//3. 遍历切片,然后按照key来输出map的值
var keys []int
for k, _ := range map1 {
keys = append(keys, k)
}
//排序
sort.Ints(keys)
//根据排序后的切片输出map中key对应的value
for _, k := range keys {
fmt.Printf("map1[%v]=%v \n", k, map1[k])
}
}
②使用细节
- map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
- map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长 键值对(key-value)
- map 的 value 也经常使用 struct 类型,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体
2 结构体(面向对象特性)
2.1 概念
Golang中面向对象的特点:
-
Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
-
Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
-
Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
-
Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
-
Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。后面同学们会充分体会到这个特点。也就是说在Golang 中面向接口编程是非常重要的特性。
结构体变量(实例)在内存的布局(重要!)
2.2 声明、使用及注意细节
①声明
type 结构体名称 struct {
field1 type
field2 type
}
- 从概念或叫法上看: 结构体字段 = 属性 = field (即授课中,统一叫字段)
- 字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性
②使用
package main
import (
"fmt"
_ "sort"
)
//首字母大写表明公有
type Person struct {
Name string
Age int
slice []int //切片
map1 map[string]string //map
}
func main(){
//1. 直接声明
var person Person
person.slice = make([]int, 5)
person.map1 = make(map[string]string)
fmt.Println("person=", person)
//2. 直接声明并初始化 {}
var person2 Person = Person{"jack", 20, make([]int, 5), make(map[string]string)}
fmt.Println("person2=", person2)
// person3 := Person{"jucy", 0, make([]int, 5), make(map[string]string)}
// fmt.Println("person3=", person3)
//3. &
var p3 *Person = new(Person)
//因为p3是一个指针, 因此:
//(*)p3.Name = "smith" 也可以这样写 p3.Name = "smith"
//go的设计者为了程序员使用方便,所以在底层帮我们做了封装,底层会对 p3.Name = "smith"进行处理,会变为(*p3).Name
(*p3).Name = "smith"
p3.Name = "john"
fmt.Println("person3=", p3)
//4. {}
var p4 *Person = &Person{}
(*p4).Name = "scott"
p4.Name = "scott~~"
fmt.Println("person4=",*p4)
}
③注意细节
- 字段声明语法同变量,示例:字段名 字段类型
- 字段的类型可以为:基本类型、数组或引用类型
- 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样:
布尔类型是 false ,数值是 0 ,字符串是 “”。
数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
- 不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个, 结构体是值类型。
注意:
- 结构体的所有字段在内存中是连续的
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
- 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
- struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
序列化使用场景:
package main
import (
"fmt"
"encoding/json"
)
//前后端传输过程中,前端更希望字段名为小写,但是go语言如果为了让其他包能访问到字段只能
//首字母大写,因此就需要struct的tag来完成
type Monster struct {
Name string `json:"name"` //`json:"name"`就是strcut tag
Age int `json:"age"`
Skill string `json:"skill"`
}
func main(){
monster := Monster{"牛魔王", 500, "芭蕉扇~"}
//将monster变量序列化为json格式字符串
//json.Marshal 函数中使用反射
jsonStr, err := json.Marshal(monster)
if err != nil {
fmt.Println("json处理错误 ", err)
}
//jsonStr {"name":"牛魔王","age":500,"skill":"芭蕉扇~"}
fmt.Println("jsonStr", string(jsonStr))
}
3 方法
3.1 介绍
在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名…),Person 结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法才能完成。
- Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct。
3.2 方法的声明和调用
package main
import (
"fmt"
)
type Person struct {
Name string
}
//给Person绑定一个方法[绑定之后只有Person才能使用]
func (p Person) test(){
fmt.Println("test() name=", p.Name)
}
func main(){
var p Person
p.Name = "tom"
p.test()//调用方法
}
- test 方法和 Person 类型绑定
- test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调
用- func (p Person) test() {}… p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非常相似。
- p 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以
3.3 方法的调用和传参机制(重要)
方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。下面我们举例说明。
例如:getSum的方法过程
说明:
- 在通过一个变量去调用方法时,其调用机制和函数一样
- 不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地址拷贝)
3.4 注意事项
①声明和定义
func (recevier type) methodName(参数列表) (返回值列表){
方法体
return 返回值
}
- 参数列表:表示方法输入
- recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
- receiver type : type 可以是结构体,也可以其它的自定义类型
- receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一功能代码块
- return 语句不是必须的。
②注意事项和细节
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
- 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
- 如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出【类比java中重写了toString方法】
3.5 方法和函数的区别
- 调用方式不一样
函数的调用方式: 函数名(实参列表)
方法的调用方式: 变量.方法名(实参列表)- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法(如 struct 的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
package main
import (
"fmt"
)
type Person struct {
Name string
}
//对于方法(如:struct的方法)
//接收者为值类型时,可以直接使用指针类型的变量调用方法,反过来也同样可以
//1. 和结构体类型绑定
func (p Person) test03(){
p.Name = "jack"
fmt.Println("test03()=", p.Name)
}
//2. 和指针类型绑定
func (p *Person) test04(){
p.Name = "mary"
fmt.Println("test04()=", p.Name)
}
func main(){
var p Person
p.Name = "tom"
p.test03()
fmt.Println("main() p.Name=", p.Name) //tom
(&p).test03() //从形式上看是传入地址,但是本质上仍然是值拷贝【因为test03方法是和结构体绑定的】
fmt.Println("main() p.Name=", p.Name) //tom
(&p).test04()
fmt.Println("main() p.Name=", p.Name) //mary
p.test04() //等价(&p).test04(),从形式上是传入值类型,但是本质仍然是地址拷贝
//【调用方不决定,决定权在调用的方法和什么绑定】
}
总结:
- 不管调用形式如何,真正决定是值拷贝还是地址拷贝,看这个方法是和哪个类型绑定.
- 如果是和值类型,比如 (p Person) , 则是值拷贝, 如果和指针类型,比如是 (p *Person) 则是地址拷贝。
3.6 工厂模式(类比get、set)
在java中为了访问私有的属性,通常会提供对应的get、set方法,但是Go中并没有;因此我们可以使用工厂模式来达到类似的效果
- Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。
一个结构体的声明是这样的:
package model
type Student struct {
Name string...
}
因为这里的Student 的首字母S 是大写的,如果我们想在其它包创建 Student 的实例(比如main 包), 引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的, 比如 是 type student struct {…} 就不不行了,怎么办—> 工厂模式来解决.
- model包中定义一个结构体student
main.go:
package model
//定义一个私有的结构体
//Name字段是共有的,age字段是私有的
type student struct {
Name string
age int
}
//工厂模式:类比构造函数
//返回Student指针类型
func NewStudent(name string, age int) *student{
return &student{
Name: name,
age: age,
}
}
//工厂模式定义student对应的方法:类比get、set方法
func (stu *student) GetAge() int{
return (*stu).age
}
func (stu *student) SetAge (age int) {
(*stu).age = age
}
- 在main包下使用student
hello.go
package main
import (
"fmt"
"go_code/project01/main/model"
)
func main(){
//创建一个student
var stu = model.NewStudent("tom", 20)
stu.SetAge(17)
//因为Name在model中是首字母大写表明是公有的,因此可以直接访问;而age需要通过GetAge函数实现
//tom 17
fmt.Println(stu.Name, stu.GetAge())
}
4 面向对象
4.1 面向对象练习题(封装)
- 创建程序,在 model 包中定义 Account 结构体:在 main 函数中体会 Golang 的封装性。
- Account 结构体要求具有字段:账号(长度在 6-10 之间)、余额(必须>20)、密码(必须是六
- 通过 SetXxx 的方法给 Account 的字段赋值。(同学们自己完成
- 在 main 函数中测试
- 定义account结构体
package model
import (
"fmt"
)
type account struct {
accountNo string //账户
Balance float64 //余额
Password string
}
//使用工厂方法完成构造
func NewAccount(accountNo string, Balance float64, Password string) *account {
if len(accountNo) < 6 || len(accountNo) > 10 {
fmt.Println("账号必须在6-10之间...")
return nil
}
if Balance < 20 {
fmt.Println("余额不能少于20...")
return nil
}
if len(Password) != 6 {
fmt.Println("密码必须是6位..")
return nil
}
account := account{
accountNo: accountNo,
Balance: Balance,
Password: Password,
}
fmt.Println("创建账户成功~~~")
return &account
}
//定义get方法[与结构体绑定]
func (a account) GetAccountNo() string{
return a.accountNo
}
//set方法[与指针绑定]
func (a *account) SetAccountNo(accountNo string){
(*a).accountNo = accountNo
}
- 使用
package main
import (
"fmt"
"go_code/project01/main/model"
)
func main(){
//创建一个账户
var account = model.NewAccount("4131341324", 35.5, "348291")
fmt.Println("账户No=", account.GetAccountNo())
account.SetAccountNo("6666666666")
fmt.Println("新的账户No=", account.GetAccountNo())
}
结果:
4.2 面向对象(继承)
①概念
继承的优点:
- 代码的复用性提高了
- 代码的扩展性和维护性提高了
type Goods struct { Name string Price int
}
type Book struct {
//这里就是嵌套匿名结构体 Goods Writer string
Goods
}
②使用及注意点
继承的深入讨论:
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法, 都可以使用。【举例说明】
package main
import (
"fmt"
_ "go_code/project01/main/model"
)
type A struct {
Name string
age int
}
//公有
func (a *A) SayOk(){
fmt.Println("A SayOk ", a.Name)
}
//私有
func (a *A) hello(){
fmt.Println("A hello", a.Name)
}
type B struct {
//B继承了A
A
}
func main(){
var b B
b.A.Name = "tom"
//b.Name = "jack" //两种写法均可
b.age = 12
b.SayOk()
b.hello()
}
- 匿名结构体字段访问可以简化
b.A.Name = "tom"
//b.Name = "jack" //两种写法均可
对上面的代码小结
(1) 当我们直接通过 b 访问字段或方法时,其执行流程如下比如 b.Name
(2) 编译器会先看 b 对应的类型有没有 Name, 如果有,则直接调用 B 类型的 Name 字段
(3) 如果没有就去看 B 中嵌入的匿名结构体 A 有没有声明 Name 字段,如果有就调用,如果没有继续查找…如果都找不到就报错.
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分【举例说明】
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错。【举例说明】
- 如果一个 struct 嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
package main
import (
"fmt"
_ "go_code/project01/main/model"
)
type Goods struct {
Name string
Price float64
}
type Brand struct {
Name string
Address string
}
type TV struct {
Goods
Brand
}
type TV2 struct {
*Goods
*Brand
}
func main(){
//嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
tv := TV{Goods{"电视机", 5000.99}, Brand{"海尔", "山东"}}
tv2 := TV{
Goods{
Price: 6999.99,
Name: "电视机002",
},
Brand{
Name: "夏普",
Address: "北京",
},
}
fmt.Println("tv=", tv)
fmt.Println("tv2=", tv2)
tv3 := TV2{
&Goods{
Name: "电视机003",
Price: 79999.99,
},
&Brand{
Name: "长虹",
Address: "四川",
},
}
fmt.Println("tv3=", *(tv3.Goods), *(tv3.Brand))
}
7. 结构体的匿名字段是基本数据类型,如果需要有多个 int 的字段,则必须给 int 字段指定名字
package main
import (
"fmt"
_ "go_code/project01/main/model"
)
type Monster struct {
Name string
Age int
}
type E struct {
Monster
int //匿名字段的基本数据类型
n int
}
func main(){
//匿名字段是基本数据类型的使用
var e E
e.Name = "狐狸精"
e.Age = 300
e.int = 20
e.n = -50
//e= {{狐狸精 300} 20 -50}
fmt.Println("e=",e)
}
③多重继承
如一个 struct 嵌套了多个匿名结构体,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承。
多重继承细节说明
-
如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。【案例演示】
-
为了保证代码的简洁性,建议大家尽量不使用多重继承