Go基础4

目录

一. 结构体

1.1 结构体的定义

1.1.1 匿名结构体

1.2 结构体指针

1.3 结构体初始化

1.4 结构体内存布局

1.5 构造函数

1.6 方法和接收者

1.7 值和指针接收者

1.8 任意类型添加方法

1.9 结构体的匿名字段

1.10 结构体嵌套

1.11 匿名嵌套结构体

1.12 嵌套结构体的字段名冲突

1.13 结构体模拟实现继承

1.14 练习

1.14.1 学生管理系统(函数版)

1.15 结构体与JSON序列化

二. 接口

2.1 接口的类型

2.2 值接收者和指针接收者

2.2.1 值接收者

2.2.2 指针接收者

2.3 类型与接口的关系

2.4 空接口

2.5 类型断言


一. 结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

1.1 结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

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
[篮球 足球 双色球]

1.1.1 匿名结构体

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}

1.2 结构体指针

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}

1.3 结构体初始化

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}

1.4 结构体内存布局

结构体占用一块连续的内存。

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

1.5 构造函数

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 男}

1.6 方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self

方法的定义格式如下:

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}
  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为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()
}

老王:汪汪汪~

方法与函数的区别是:函数不属于任何类型,方法属于特定的类型。

1.7 值和指针接收者

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}

什么时候应该使用指针类型接收者:

  1. 需要修改接收者中的值

  2. 接收者是拷贝代价比较大的大对象

  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

1.8 任意类型添加方法

在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类型

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

1.9 结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

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

适用于字段比较少且简单的场景(不常用)

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

1.10 结构体嵌套

一个结构体中可以嵌套包含另一个结构体或结构体指针。

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
{老王 汉中 陕西省汉中市洋县}
老王

1.11 匿名嵌套结构体

结构体中嵌套的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

当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找。

1.12 嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

//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
}

1.13 结构体模拟实现继承

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岁
名字叫老徐的狗狗在吃饭

1.14 练习

1.14.1 学生管理系统(函数版)

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("滚 滚 滚 ~ ~ ~")
		}
	}

}

1.15 结构体与JSON序列化

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 接口的类型

接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。

相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么。

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

2.2 值接收者和指针接收者

2.2.1 值接收者

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)

}

{老王 小鱼}
&{胖子 小虾}

使用值接收者实现接口之后,不管是结构体是值类型还是对应指针类型的变量都可以赋值给该接口变量。  

2.2.2 指针接收者

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)

}

&{老王 小鱼}
&{胖子 小虾}

指针接收者实现接口只能存结构体指针类型的变量。

2.3 类型与接口的关系

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() {

}

2.4 空接口

空接口是指没有定义任何方法的接口类型。因此任何类型都可以视为实现了空接口。也正是因为空接口类型的这个特性,空接口类型的变量可以存储任意类型的值。

定义格式如下,没要必要起名字。

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

2.5 类型断言

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 语言中接口是一个非常重要的概念和特性,使用接口类型能够实现代码的抽象和解耦,也可以隐藏某个功能的内部实现,但是缺点就是在查看源码的时候,不太方便查找到具体实现接口的类型。

猜你喜欢

转载自blog.csdn.net/hpl980342791/article/details/125438251