目录
Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
Go语言中通过struct来实现面向对象。
使用type和struct关键字来定义结构体,具体代码格式如下:
- type 类型名 struct {
- 字段名 字段类型
- 字段名 字段类型
- …
- }
类型名:标识自定义结构体的名称,在同一个包内不能重复。
字段名:表示结构体字段名。结构体中的字段名必须唯一。
字段类型:表示结构体字段的具体类型。
- package main
-
- import "fmt"
-
- // 自定义结构体
- type person struct {
- name string
- age int
- sex string
- hobby []string
- other string
- }
-
- func main() {
- var p person
- p.name = "老王"
- p.age = 35
- p.sex = "男"
- p.hobby = []string{"篮球", "足球", "双色球"}
- p.other = "暂无"
-
- fmt.Println(p)
- fmt.Println(p.age) // 输出相应的属性
- fmt.Println(p.hobby)
- }
-
- {老王 35 男 [篮球 足球 双色球] 暂无}
- 35
- [篮球 足球 双色球]
- package main
-
- import (
- "fmt"
- "reflect"
- )
-
- func main() {
- // 匿名结构体
- var s struct {
- name string
- age int
- }
- s.age = 45
- s.name = "很漂亮"
-
- fmt.Println(reflect.TypeOf(s), s)
-
- }
-
- struct { name string; age int } {很漂亮 45}
- package main
-
- import "fmt"
-
- // 定义一个结构体
- type person struct {
- name, sex string
- age int
- }
-
- // go语言中函数的参数永远是拷贝
- func f1(x person) {
- x.age = 18 // 也就是说这里修改的是副本的age
- }
-
- /* 假如要想在函数中修改变量,就要使用指针 */
- func f2(x *person) {
- // (*x).age = 18 // 根据内存地址找原来的变量,进行修改
- x.age = 18 // 同上,语法糖,自动根据指针找到对应的变量
- }
-
- func main() {
- var p person // 声明一个person类型的变量p
- p.age = 22
- p.name = "老王"
- p.sex = "男"
- f1(p)
- fmt.Println(p)
-
- f2(&p) // 传入内存地址
- fmt.Println(p)
- }
-
- {老王 男 22}
- {老王 男 18}
- package main
-
- import (
- "fmt"
- )
-
- type person struct {
- name string
- age int
- }
-
- func main() {
- p1 := person{ // 初始化及赋值
- name: "很漂亮",
- age: 20,
- }
- fmt.Println(p1)
-
- p2 := person{ // 只使用值进行初始化及赋值(要按照顺序进行赋值)
- "老王",
- 35,
- }
- fmt.Println(p2)
-
- p3 := &person{ // 指针类型的结构体
- "胖子",
- 24,
- }
- fmt.Println(p3)
- }
-
- {很漂亮 20}
- {老王 35}
- &{胖子 24}
结构体占用一块连续的内存。
- type test struct {
- a int8
- b int8
- c int8
- d int8
- }
- n := test{
- 1, 2, 3, 4,
- }
- fmt.Printf("n.a %p\n", &n.a)
- fmt.Printf("n.b %p\n", &n.b)
- fmt.Printf("n.c %p\n", &n.c)
- fmt.Printf("n.d %p\n", &n.d)
-
- n.a 0xc0000a0060
- n.b 0xc0000a0061
- n.c 0xc0000a0062
- n.d 0xc0000a0063
Go语言的结构体没有构造函数,我们可以自己实现。因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
- package main
-
- import "fmt"
-
- type s struct {
- name string
- age int
- sex string
- }
-
- // 在Go语言中,构造函数都是以new开始的
- // 当结构体比较大时,尽量使用结构体指针,减少程序的开销
- func newPerson(name, sex string, age int) *s {
- return &s{
- name: name,
- age: age,
- sex: sex,
- }
- }
-
- func main() {
- s1 := newPerson("hpl", "男", 23)
- s2 := newPerson("老王", "男", 34)
- fmt.Println(s1, s2)
- }
-
- &{hpl 23 男} &{老王 34 男}
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。
方法的定义格式如下:
- func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
- 函数体
- }
接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。
接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
方法名、参数列表、返回参数:具体格式与函数定义相同。
- package main
-
- import "fmt"
-
- type dog struct {
- name string
- }
-
- // 方法
- func (d dog) d1() {
- fmt.Printf("%s:汪汪汪~", d.name)
- }
-
- func main() {
- dd := dog{
- "老王",
- }
- dd.d1()
- }
-
- 老王:汪汪汪~
方法与函数的区别是:函数不属于任何类型,方法属于特定的类型。
- package main
-
- import "fmt"
-
- type dog struct {
- name string
- age int
- }
-
- // 使用值接收者(传值拷贝进去)
- func (d dog) f1() {
- d.age++
- }
-
- // 使用指针接收者(传指针进去)
- func (d *dog) f2() {
- // (*d).age++
- d.age++
- }
-
- func main() {
- d1 := dog{"老王", 12}
- d1.f1()
- fmt.Println(d1)
-
- d1.f2()
- fmt.Println(d1)
-
- }
-
- {老王 12}
- {老王 13}
什么时候应该使用指针类型接收者:
需要修改接收者中的值
接收者是拷贝代价比较大的大对象
保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 如:基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
- package main
-
- import (
- "fmt"
- "reflect"
- )
-
- type myInt int // 自定义一个类型
-
- func (m myInt) hello() {
- fmt.Println("我是一个myInt类型")
- }
-
- func main() {
- m := myInt(100)
- m1 := 200
- fmt.Println(reflect.TypeOf(m))
- fmt.Println(reflect.TypeOf(m1))
- m.hello()
-
- }
-
- main.myInt
- int
- 我是一个myInt类型
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
- package main
-
- import "fmt"
-
- // 匿名字段
- type person struct {
- string
- int
- }
-
- func main() {
- p1 := person{
- "老王",
- 34,
- }
- fmt.Println(p1)
- fmt.Println(p1.string)
- fmt.Println(p1.int)
- }
-
- {老王 34}
- 老王
- 34
适用于字段比较少且简单的场景(不常用)
注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
一个结构体中可以嵌套包含另一个结构体或结构体指针。
- package main
-
- import "fmt"
-
- // 结构体的嵌套
- type introduce struct {
- name string
- city string
- address string
- }
-
- type person struct {
- age int
- addr introduce
- }
-
- type company struct {
- addr introduce
- }
-
- func main() {
- p1 := person{
- age: 23,
- addr: introduce{
- name: "老王",
- city: "汉中",
- address: "陕西省汉中市洋县",
- },
- }
- fmt.Println(p1)
- fmt.Println(p1.age)
- fmt.Println(p1.addr)
- fmt.Println(p1.addr.name)
- }
-
- {23 {老王 汉中 陕西省汉中市洋县}}
- 23
- {老王 汉中 陕西省汉中市洋县}
- 老王
结构体中嵌套的person结构体也可以采用匿名字段的方式
- package main
-
- import "fmt"
-
- type address struct {
- name string
- city string
- }
-
- type person struct {
- age int
- address // 匿名嵌套体
- }
-
- type company struct {
- addr address
- }
-
- func main() {
- p1 := person{
- age: 23,
- address: address{
- name: "hpl",
- city: "汉中",
- },
- }
- fmt.Println(p1)
- fmt.Println(p1.address)
- fmt.Println(p1.age)
- fmt.Println(p1.name)
- }
-
- {23 {hpl 汉中}}
- {hpl 汉中}
- 23
- hpl
当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。
嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。
- //Address 地址结构体
- type Address struct {
- Province string
- City string
- CreateTime string
- }
-
- //Email 邮箱结构体
- type Email struct {
- Account string
- CreateTime string
- }
-
- //User 用户结构体
- type User struct {
- Name string
- Gender string
- Address
- Email
- }
-
- func main() {
- var user3 User
- user3.Name = "沙河娜扎"
- user3.Gender = "男"
- // user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
- user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
- user3.Email.CreateTime = "2000" //指定Email结构体中的CreateTime
- }
- package main
-
- import "fmt"
-
- type animal struct {
- name string
- }
-
- // 给animal实现一个eat的方法
- func (a animal) eat() {
- fmt.Printf("名字叫%s的狗狗在吃饭\n", a.name)
- }
-
- type dog struct {
- age int
- animal
- }
-
- // 给狗实现一个叫的方法
- func (d dog) call() {
- fmt.Printf("名字叫%s的狗狗今年%d岁\n", d.name, d.age)
- }
-
- func main() {
- a1 := animal{
- name: "老王",
- }
- a1.eat()
-
- d1 := dog{
- age: 12,
- animal: animal{
- name: "老徐",
- },
- }
- d1.call()
- d1.eat()
- }
-
- 名字叫老王的狗狗在吃饭
- 名字叫老徐的狗狗今年12岁
- 名字叫老徐的狗狗在吃饭
- package main
-
- import (
- "fmt"
- "os"
- )
-
- /*
- 学生版学生管理系统
- 系统能够查看、新增、删除学生
- */
-
- // 定义学生结构体
- type student struct {
- id, age int
- name, sex string
- }
-
- var allStudent map[int]*student // 声明变量
-
- // newStudent是student类型的构造函数
- func newStudent(id, age int, name, sex string) *student {
- return &student{
- id: id,
- name: name,
- age: age,
- sex: sex,
- }
- }
-
- // 展示学生
- func showAllStudent() {
- for key, value := range allStudent {
- fmt.Printf("学号:%d, 姓名:%s\n", key, value.name)
- }
- }
-
- // 添加学生
- func addStudent() {
- // 创建一个新学生
- var (
- id, age int
- name, sex string
- )
- // 获取用户输入
- fmt.Print("请输入学生学号>>>:")
- fmt.Scanln(&id)
- fmt.Print("请输入学生姓名>>>:")
- fmt.Scanln(&name)
- fmt.Print("请输入学生年龄>>>:")
- fmt.Scanln(&age)
- fmt.Print("请输入学生性别>>>:")
- fmt.Scanln(&sex)
- // 使用构造函数创建一个学生
- newStu := newStudent(id, age, name, sex)
- // 将新创建的学生添加到 allStudent 中
- allStudent[id] = newStu
- }
-
- // 删除学生
- func deleteStudent() {
- var id int
- // 获取用户输入要删除学生的学号
- fmt.Print("请输入要删除学生的学号>>>:")
- fmt.Scanln(&id)
- delete(allStudent, id)
- fmt.Printf("\n学号是%d的学生已经被删除\n", id)
- }
-
- func main() {
- allStudent = make(map[int]*student, 50) // 初始化
-
- fmt.Println("欢迎光临学生管理系统")
- fmt.Println(`
- 1.查看所有学生
- 2.新增学生
- 3.删除学生
- 4.退出
- `)
- for {
- var choice int
- fmt.Print("请输入你想要的选项>>>:")
- fmt.Scanln(&choice)
- fmt.Printf("你选择了 %d 这个选项\n", choice)
- switch choice {
- case 1:
- showAllStudent()
- case 2:
- addStudent()
- case 3:
- deleteStudent()
- case 4:
- os.Exit(1)
- default:
- fmt.Println("滚 滚 滚 ~ ~ ~")
- }
- }
-
- }
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON键值对是用来保存JS对象的一种方式,键/值对组合中的键名写在前面并用双引号""包裹,使用冒号:分隔,然后紧接着值;多个键值之间使用英文,分隔。
- package main
-
- import (
- "encoding/json"
- "fmt"
- )
-
- type person struct {
- Name string `json:"name"`
- Age int `json:"age"`
- }
-
- func main() {
- p1 := person{
- Name: "老王",
- Age: 38,
- }
- // 序列化
- bt, err := json.Marshal(p1)
- if err != nil {
- fmt.Printf("序列化失败,错误是%v\n", err)
- }
- fmt.Println(string(bt))
-
- // 反序列化
- str1 := `{"name":"老王","age":38}`
- var p2 person
- json.Unmarshal([]byte(str1), &p2)
- fmt.Printf("%#v\n", p2)
-
- }
-
- {"name":"老王","age":38}
- main.person{Name:"老王", Age:38}
注意:反引号里面不能有空格
在Go语言中接口(interface)是一种类型,一种抽象的类型。相较于之前章节中讲到的那些具体类型(字符串、切片、结构体等)更注重“我是谁”,接口类型更注重“我能做什么”的问题。接口类型就像是一种约定——概括了一种类型应该具备哪些方法,在Go语言中提倡使用面向接口的编程方式实现解耦。
接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。
相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么。
2.1.1 接口的定义
每个接口类型由任意个方法签名组成,接口的定义格式如下:
- type 接口类型名 interface{
- 方法名1( 参数列表1 ) 返回值列表1
- 方法名2( 参数列表2 ) 返回值列表2
- …
- }
接口类型名:Go语言的接口在命名时,一般会在单词后面添加
er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
如,定义一个包含Write方法的Writer接口。
- type Writer interface{
- Write([]byte) error
- }
当你看到一个Writer接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事情。
- package main
-
- import "fmt"
-
- type animaler interface {
- sleep()
- eat(str string)
- }
-
- type dog struct {
- name string
- age int
- sex string
- }
-
- func (d dog) sleep() {
- fmt.Printf("名字叫%s的狗狗喜欢睡觉\n", d.name)
- }
-
- func (d dog) eat(str string) {
- fmt.Printf("%s今年%d岁,喜欢啃骨头\n", str, d.age)
- }
-
- func show(a animaler, str string) {
- a.eat(str)
- a.sleep()
- }
-
- func main() {
- d1 := dog{
- name: "laowang",
- age: 3,
- sex: "male",
- }
- show(d1, "哈士奇")
- }
-
- 哈士奇今年3岁,喜欢啃骨头
- 名字叫laowang的狗狗喜欢睡觉
- package main
-
- import "fmt"
-
- // 定义接口类型
- type animal interface {
- move()
- eat(string)
- }
-
- type cat struct {
- name string
- like string
- }
-
- type chicken struct {
- name string
- }
-
- // chicken结构体的两个方法
- func (c chicken) move() {
- fmt.Println("鸡只有两条腿,所以呢?跑的不快")
- }
-
- func (c chicken) eat(food string) {
- fmt.Printf("我喜欢吃%s\n", food)
- }
-
- // cat结构体的两个方法
- func (c cat) move() {
- fmt.Println("我们一起走猫步")
- }
-
- func (c cat) eat(food string) {
- fmt.Printf("小猫喜欢吃%s\n", food)
- }
-
- func main() {
- var a1 animal
-
- c1 := cat{
- name: "球球",
- like: "喜欢玩耍",
- }
- a1 = c1
- a1.eat("鱼鱼")
-
- c2 := chicken{
- name: "鸡大婶",
- }
-
- var a2 animal
-
- a2 = c2
- fmt.Printf("%T\n", a2)
-
- }
-
-
- 小猫喜欢吃鱼鱼
- main.chicken
- package main
-
- import "fmt"
-
- // 定义接口类型
- type animal interface {
- move()
- eat(string)
- }
-
- type cat struct {
- name string
- like string
- }
-
- func (c cat) move() {
- fmt.Println("走猫步....")
- }
-
- func (c cat) eat(food string) {
- fmt.Printf("猫喜欢吃%s\n", food)
- }
-
- func main() {
- var a1 animal
- c1 := cat{
- name: "老王",
- like: "小鱼",
- }
- c2 := &cat{
- name: "胖子",
- like: "小虾",
- }
- a1 = c1
- fmt.Println(a1)
- a1 = c2
- fmt.Println(a1)
-
- }
-
- {老王 小鱼}
- &{胖子 小虾}
使用值接收者实现接口之后,不管是结构体是值类型还是对应指针类型的变量都可以赋值给该接口变量。
- package main
-
- import "fmt"
-
- // 定义接口类型
- type animal interface {
- move()
- eat(string)
- }
-
- type cat struct {
- name string
- like string
- }
-
- func (c *cat) move() {
- fmt.Println("走猫步....")
- }
-
- func (c *cat) eat(food string) {
- fmt.Printf("猫喜欢吃%s\n", food)
- }
-
- func main() {
- var a1 animal
- c1 := &cat{
- name: "老王",
- like: "小鱼",
- }
- c2 := &cat{
- name: "胖子",
- like: "小虾",
- }
- a1 = c1
- fmt.Println(a1)
- a1 = c2
- fmt.Println(a1)
-
- }
-
- &{老王 小鱼}
- &{胖子 小虾}
指针接收者实现接口只能存结构体指针类型的变量。
- package main
-
- import "fmt"
-
- // 同一个结构体可以实现多个接口
- // 接口可以嵌套
-
- // 方法1
- type mover interface {
- move()
- eater
- }
-
- // 方法2
- type eater interface {
- eat(string)
- }
-
- // 结构体
- type cat struct {
- name string
- like string
- }
-
- // cat实现了move接口
- func (c *cat) move() {
- fmt.Println("走猫步....")
- }
-
- // cat实现了eat接口
- func (c *cat) eat(food string) {
- fmt.Printf("猫喜欢吃%s", food)
- }
-
- func main() {
-
- }
空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。
定义格式如下,没要必要起名字。
interface{} // 空接口
所有的类型都实现了空接口,也就是任意类型的变量都能保存到空接口中。
- package main
-
- import "fmt"
-
- // 空接口作为函数的参数
- func show(a interface{}) {
- fmt.Printf("value:%v, type:%T\n", a, a)
- }
-
- func main() {
- m1 := make(map[string]interface{}, 10)
- m1["name"] = "老王"
- m1["age"] = 35
- m1["sex"] = "男"
- m1["married"] = true
- m1["hobby"] = [...]string{"唱歌", "跳舞", "睡觉"}
- fmt.Println(m1)
-
- show(false)
- show(nil)
- show(25)
- }
-
- map[age:35 hobby:[唱歌 跳舞 睡觉] married:true name:老王 sex:男]
- value:false, type:bool
- value:<nil>, type:<nil>
- value:25, type:int
- package main
-
- import (
- "fmt"
- )
-
- // 类型断言
- func assign(v interface{}) {
- v, ok := v.(string)
- if !ok {
- fmt.Println("类型错误")
- } else {
- fmt.Printf("这就是一个string类型:%v", v)
- }
- }
-
- // 类型断言
- func assign2(v interface{}) {
- switch t := v.(type) {
- case string:
- fmt.Println("这是一个字符串,", t)
- case int:
- fmt.Println("这是一个int,", t)
- case bool:
- fmt.Println("这是一个布尔类型,", t)
- default:
- fmt.Println("这是一个其他类型,", t)
- }
- }
-
- func main() {
- assign2([...]string{"hpl"})
- }
-
- 类型错误
- 这是一个其他类型, [hpl]
在 Go 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。