Go语言基础(三)结构体和接口

0、 结构体

go 没有原生的 “类”的概念,也不支持继承等OOP的概念;Go通过结构体内嵌再配合接口比OOP有更好的灵活性和扩展性

0.1 类型别名和自定义类型

go 中有string int float bool 等基本类型,可以使用type来自定义类型。自定义类型就是定义了一个新的类型。比如 type MyInt int 就是定义了一个新的类型MyInt,它具有int的特性

0.2 类型别名

其实就是给一个类型又取了一个名字,格式:

type TypeAlias = Type

比如 runebyte就是别名:
type rune = int32
type byte=uint8

0.3 结构体

基础数据类型可以用来描述简单属性,比如var color string,类型string描述了属性color。当我们需要描述复杂属性(多个属性)时,基础数据类型就会不够用了。在java中,我们使用类 class去封装一组属性(和行为),在go中,也有接近的概念struct

结构体的定义:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

举个栗子【这和 java 的类何其相似!】:

type person struct {
	name string
	city string
	age  int8
}

这里还要特别注意一点:结构体是值类型.比如看下面这个例子:
这倒是和Java对象 传递引用不太相同。可以记一下:GO语言中传参总是副本。


type person struct {
	name string
	age  int
}

func fn4(){
	p := person{
		name: "air",
		age: 1,
	}

	fmt.Println(p) // {air 1}

	changeName(p)  // 相当于只是修改了 副本

	fmt.Println(p) // {air 1} 
}

func changeName(s person){
	s.name= "bee"
}

0.3.1 实例化

  • Go的结构体必须先实例化才会真正分配内存,i.e.,必须实例化后才能使用结构体的字段。不过这里很有意思,我们并不需要显式地 make 或 new 去实例化一个struct
  • Go的结构体也是个类型 type,我们可以 var p Person这样声明结构体类型。
func fn6(){
	var p Person
	p.age=10
	p.name="justin"
	p.city="GZ"

	fmt.Printf("%v \n", p)
	p.city = "BJ"
	fmt.Printf("%v \n", p)
}

0.3.2 匿名结构体

func fn7(){
	// 匿名结构体
	var d struct{color string;age uint8}
	d.color = "yellow" 
	d.age = 1
	fmt.Println(d)
}

0.3.3 创建指针类型结构体

可以通过 new 来初始化一个结构体,返回的是一个 指针

func fn8() {
	// p2 is a struct pointer
	var p2 = new(Person)
	fmt.Printf("%T \n", p2)
	fmt.Printf("%#v ", p2)
}

0.3.4 取结构体的地址实例化

通过 & 来实例化一个结构体

func fn9(){
	var p3 = &Person{} // 对一个结构体 取址操作,相当于对这个 结构体进行了实例化,但是并没有初始化, p3 中的所有属性都是 零值
	fmt.Printf("%T \n", p3)
	fmt.Printf("%#v \n", p3)

	p3.age=10	
	p3.city="GZ"
	p3.name="justin"

	fmt.Printf("%#v \n", p3)
}

有人可能对 p3.age=10的表达感到困惑: p3 是一个 指针,那为啥可以直接执行p3.age呢,正常逻辑不应该是*p3.age=10 吗?
这其实 这是Go的一个语法糖,本质上其实是 : (*p3).age = 10

0.3.5 使用KV对来初始化

这里,要稍微注意一下,实例化 和 初始化 还是两个分开的概念。

1、使用 KV对对结构体进行初始化, k – 结构体的属性 ,v – 字段的值

func fn10(){
	var p4 = Person{
		name:"jim",
		age:10,
		city:"BZ",
	}

	fmt.Printf("%#v ", p4)
}

2、使用& 取址来对一个结构体进行初始化。假如一些字段没有初始值,没有初始值的字段的值就是 零值。&相当于对该结构体进行了一次new 实例化操作

func fn11() {
	var p5 = &Person{ // 使用取址对一个结构体进行初始化
		name: "pat",      //这其实是一个语法糖 ,底层是 (*p5).name="pat"
		age:  11,
	}
	fmt.Printf("%#v \n", p5) //&main.Person{name:"pat", age:11, city:""}
	fmt.Printf("%T \n", p5)   // *main.Person
	fmt.Prinfg("%v \n",p5.name) 
	}

3、使用值的列表初始化
这种方式:

  • 必须初始化所有字段
  • 初始值填充顺序必须和字段声明顺序一致
  • 这种方式和 KV对初始化方式不能混用

0.3.6 结构体内存布局

go中的结构体占用一块连续的内存,而空结构体是不占用内存的

比如,这个 test结构体,其初始化后,字段占用的内存都是对齐的。

type test struct{
	a int8;
	b int8;
	c int8;
	d int8
}

func fn12(){
	var t = test{
		a:1,
		b:1,
		c:1,
		d:1,
	}

	//t.a 0xc0000a2058
	//t.b 0xc0000a2059
	//t.b 0xc0000a205a
	//t.b 0xc0000a205b
	fmt.Printf("t.a %p \n", &t.a)
	fmt.Printf("t.b %p \n", &t.b)
	fmt.Printf("t.b %p \n", &t.c)
	fmt.Printf("t.b %p \n", &t.d)
}

看这个case:空结构体是不占用空间的。

func fn13() {
	a:= empty{}
	fmt.Println(unsafe.Sizeof(a))  //0 【这里的 unsafe.Sizeof()?】
}

type empty struct{

}

0.3.7 构造函数

在Java中,类初始化是需要执行构造函数的。但是在 go 中并没有原生的构造函数概念,不过,我们可以自己动身定义一个 结构体的构造函数。这样做的最大好处在于:struct类型是值传递,传参struct会引发拷贝,当struct 较大时,性能有损。

func fn15(){
	i := newStudent("dog", 1)
	fmt.Println(*i) //{dog 1}
	fmt.Println(i) //&{dog 1}
}
// 这是一个 构造函数(自定义的),返回的是 指针
func newStudent(name string, age int) *student{
	return &student{
		name:name,
		age: age,
	}
}

type student struct {
	name string
	age  int
}

0.3.8 方法和接收者

  • 方法(method)是作用于特定类型变量的函数,我们称这种特定类型变量为 接收者(receiver)。接收者,有点类似其他语言中的this 或 self
  • 方法和函数的区别在于:函数不属于任何类型,方法却属于特定的类型
  • go中的方法和 java中的方法是完全不同的概念,java中的方法更像是 go中的函数。如果把结构体比作一个类型,方法就是类中的方法(说法不严谨,但有那么点意思在里面)

func fn16() {
	s := newStudent("ace", 10)	// 把 s 当成一个java对象
	e := s.learn("english") // learn()像是一个 java 方法
	fmt.Println(e)
}

// 方法的声明 
// 按照官方的推荐,接收者 s 应该是 类型的首字母小写
// (s student) 中的 student 是说 接收者类型为 student
// 接收者类型,既可以是 指针,也可以是非指针类型
func (s student) learn(name string) string {
	fmt.Println(s.name, " --> ", name)
	return name
}

func newStudent(name string, age int) *student {
	return &student{
		name: name,
		age:  age,
	}
}

type student struct {
	name string
	age  int
}

0.3.9 指针类型的receiver

指针类型的receiver由一个结构体的指针组成;通过指针调用方法,可以修改指针对应的实例的属性值。这种场景下, 越发显得 receiver 和 java中的this 接近。

func fn17() {
	s := newStudent("amy", 11)
	s.setAge(2)
	fmt.Println(s.age)
}
// receiver的类型是一个 指针
func (s *student) setAge(age int) {
	s.age = age //和java的 setter 何其相似,s *student 和 java 的this 多像
}

//结构体的构造函数,返回的是 一个 指针
func newStudent(name string, age int) *student {
	return &student{
		name: name,
		age:  age,
	}
}

type student struct {
	name string
	age  int
}

0.3.10 值类型的receiver

当方法作用于值类型receiver时,Go语言会在代码运行时将receiver复制一份。在值类型receiver的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

func fn18(){
	s:=newStudent("bob",12)
	fmt.Println(s.age) //12
	s.setAge2(20) //  here it does not work
	fmt.Println(s.age) //12
}

func (s student) setAge2(age int) {
	s.age = age 
}

//结构体的构造函数,返回的是 一个 指针
func newStudent(name string, age int) *student {
	return &student{
		name: name,
		age:  age,
	}
}

type student struct {
	name string
	age  int
}

0.3.11 指针类型 receiver 的使用场景

  • 需要修改接收者中的值
  • 接收者是拷贝代价比较大的大对象
  • 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

0.3.12 任意类型添加方法

方法不是 结构体类型的专利,go中任意类型都可以添加方法。 值得注意的是: 非本地类型不能定义方法,即:不能给别的包的类型定义方法。

// MyInt 是 自定义类型
// 对 基础类型 添加方法,感觉有点像是 Java中的 包装类
type MyInt int

func (m MyInt)foo(){
	fmt.Println("foo...")
	fmt.Printf("%v \n", m)
}

func fn19(){
	var i MyInt
	i = 8
	i.foo()  //打印了 8
}

0.3.13 结构体匿名字段

声明结构体时,我们可以只提供 字段类型,不提供字段名。本质上,go提供了和字段类型同名的字段名。不过个人不是很建议采用这种做法,这样会造代码不是很清晰。

type Dog struct{
	string
	int
}

func fn20(){
	d := Dog{
		"boy",11,
	}
	fmt.Println(d) //{boy 11}
}

0.3.14 嵌套结构体

学到这里,就会发现,go的结构体和java的类 十分类似了,结构体就像是go提供的OOP 解决方案。
所谓的嵌套结构体,就是 一个结构体的字段类型并不是一个基础类型,而是另一个结构体类型。
如果再仔细点,就会发现下面示例中的 User实例和 JSon数据结构有着很大的相似性。实际会不会有啥关联呢?我们且看。

type Address struct {
	Province string
	City     string
	County   string
}

type User struct{
	Name string
	Gender string
	Address Address
}

func fn22(){
	u := User{
		Name:"ht",
		Gender:"male",
		Address:Address{	// 注意这里的语法的 第二个的 Address 
			Province:"GD",
			City:"GZ",
			County:"HZ",
		},
	}
	fmt.Println(u)
}	

0.3.15 嵌套匿名字段

嵌套匿名字段:只有字段类型,没有字段名(实际上将字段类型作为了字段名)

0.3.16 嵌套结构体字段名冲突

当嵌套结构体内部存在重名字段时,为了避免歧义,需要指定具体的内嵌结构体字段名。

0.3.17 结构体继承

go 使用了“组合”的方式来继承,而不是Java那样使用了 extend 关键字。

type Animal struct{
	name string
}
func (a *Animal) move(){
	fmt.Printf("%v moves \n", a.name)
}

type Cat struct{
	Color string
	*Animal	// 匿名字段 ,注意这里是指针.同时注意,匿名字段,匿掉的是 字段,不是类型
}

func (c *Cat) showColor(){
	fmt.Printf("the cat is of color:%v",c.Color)
}
func fn23(){
	c := &Cat{
		Color: "yellow",
		Animal: &Animal{	// 注意这里要 传入指针变量
			name: "lily",
		},
	}
	fmt.Printf("cat is:%v \n",c.name)
	c.move()	//这是个继承来的方法
	c.showColor()	// 这是 原本自身的方法
}

0.3.18 结构体字段的可见性

结构体字段首字母是大写开头,表示可以公开访问;小写访问,则仅包内可见。坦白说,这种设计有点囧。大写还是小写,其实是很容易笔误的。

0.3.19 结构体和JSON

func fn1(){
	c:= &Class{
		Title: "class-1",
		Students: make([]*Student, 0, 30),	// 初始化一个切片
	}
	for i := 0; i < 2; i++ {
		s := Student{
			ID:c.Title + strconv.Itoa(i),
			Gender: "male",
			Name: fmt.Sprintf("stu%02d",i),
		}
		c.Students = append(c.Students, &s)
	}	
	fmt.Printf("%T \n", c)  	//*main.Class 

	data,ex := json.Marshal(c)
	if ex !=nil {
		fmt.Println(ex)
	}

	fmt.Printf("json值类型 %T \n", data)  // []uint8
	fmt.Printf("json内容:%s \n",data)	

	
	jsonStr:= `{"Title":"class-1","Students":[{"ID":"class-10","Gender":"male","Name":"stu00"},{"ID":"class-11","Gender":"male","Name":"stu01"}]}`
	c1 := &Class{}
	ex1 := json.Unmarshal([]byte(jsonStr), c1) // []byte(jsonStr) 这样去取字符串对应的byte数组
	if ex1 !=nil {
		fmt.Println(ex1)
	}
	fmt.Printf("反序列化结果%#v\n", *c1)
}

type Student struct{
	ID string
	Gender string
	Name string
}

type Class struct{
	Title string
	Students []*Student // 这是一个切片,元素类型是 指针
}

0.3.20 结构体标签 tag

结构体标签tag是结构体的元信息,运行时可以反射拿出来。tag 在结构体字段后面定义,如 【 key1:"value1" key2:"value2"
注意:

  • 标签在解析时容错能力极差,即使 k v 中间加多个空格,编译运行都OK,但反射时就是拿不到值。所以要严格遵守tag的规则。
func fn2(){
	c := Cat{
		name: "amy",
		Color: "pink",
		ID: 1024,
	}
	data,error := json.Marshal(c)
	// {"Color":"pink","id":1024} 
	// 1. id 变成了 小写
	// 2. name 没被序列化
	// 3. 默认的情况下,字段名(Color)变成了json的key
	if error != nil {
		fmt.Println("json parsed failed",error)
		return
	}
	fmt.Printf("json result:%s \n",data)
}
type Cat struct{
	name string // 这个是私有字段,因为字段的首字母是 小写的。私有字段仅当前包内可见,因此json包不能访问
	Color string	// json 的序列化默认是使用字段名作为key
	ID  int `json:"id"` // 通过指定tag实现json序列化该字段时 的 key
}

0.3.21 小tip

因为slice和map这两种数据类型都包含了指向底层数据的指针,因此复制时要特别注意。

type Pig struct {
	name   string
	age    int8
	dreams []string
}

func (p *Pig) setDreams(dreams []string) {
	p.dreams =dreams	
}

func fn3() {
	p := Pig{
		name: "bob",
		age: 1,
	}
	dreams:=[]string{"sleep", "eat"}
	p.setDreams(dreams)
	fmt.Println("pig:",p)  // {bob 1 [sleep eat]}
  // 这里我们修改了 切片的(底层数组)的值,导致数组元素变化了,但这 未必是预期的
  // 正确的做法:在方法中传入slice的拷贝对结构体进行赋值
	dreams[0] = "sleep more"
	fmt.Println("pig2:", p)  // {bob 1 [sleep more eat]}
}

正确的写法:

type Pig struct {
	name   string
	age    int8
	dreams []string
}

func (p *Pig) setDreams(dreams []string) {
	p.dreams = make([]string,len(dreams))
	copy(p.dreams,dreams)
}

func fn3() {
	p := Pig{
		name: "bob",
		age: 1,
	}
	dreams:=[]string{"sleep", "eat"}
	p.setDreams(dreams)
	fmt.Println("pig:",p)  // {bob 1 [sleep eat]}

	dreams[0] = "sleep more" // 即使这里我们修改了 切片的(底层数组)的值,也不会导致数组元素变化
	fmt.Println("pig2:", p)  // {bob 1 [sleep eat]}
}

一、接口

Go中的 接口是一组 method的集合,是duck type programming的一种实现。接口不关心属性(数据),只关心行为(方法)

Go的接口本质上也是一种类型

1.1 为啥有接口

func fn5(){
	c:=Cat{}
	d:=Dog{}
	fmt.Printf("cat :%v \n",c.Say())
	fmt.Printf("dog :%v \n",d.Say())
}

type Cat struct{}

func (c Cat) Say() string{
	return "meow"
}

type Dog struct{}
func (d Dog) Say() string{
	return "www"
}

这个demo,DogCat都是有 Say()的行为(方法),代码重复。如何把这种Say的行为抽象出来?这就是接口的意义了。

这种场景随处可以:

  • 接入支付功能时除了支付宝、还有微信、银联等,支付这种行为可以抽象成接口
  • 三角形、四边形都能计算面积,那么把三角形、四边形都抽象成“图形”

接口区别于Go中的其他类型,接口是一种抽象类型。看到一个接口的值,我们不知道接口它是啥,只知道它能做啥

1.2 接口定义

Go提倡 面向接口编程

接口定义如:
type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    …
}
  • Go语言的接口在命名时,一般会在单词后面添加er。从这种命名也可看出来,go的接口设计,是 duck type programming的体现。er表示 某种行为的实施者。看例子中的writer接口,说明对象只要实现了Write方法,那就是Write行为的实施者
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略

举个栗子:

type writer interface{
    Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

1.3 如何实现接口

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。 Go的这种设计,具有超大的自由度。可以看到,对象和接口其实并没有语法上的关联。在Java中,一个类需要实现接口,这个类的实例(对象)才和接口发生关系。

type sayer interface{
	say()
}

type dog struct{}
type cat struct{}
func (d dog) say(){	//dog 有 say()方法,相当于实现了sayer接口
	fmt.Println("dogggg")
}
func (c cat) say(){  //cat 有 say()方法,相当于实现了sayer接口
	fmt.Println("catttt")
}

1.4 接口类型变量

如果只是看上一节的例子,其实并不感觉到接口有啥了不起的。。接口的作用在于:接口类型变量能够接收所有实现这个接口的实例

func fn6(){
	var x sayer
	a:=cat{}
	b:=dog{}

	x = a
	x.say()
	x = b
	x.say()
}

1.5 值接收者和指针接收者实现接口的区别

上个例子:

type Mover interface{
	move()
}
type dog struct{}
  • 首先看值接收者实现接口
func (d dog)move(){
	fmt.Println("dog moves")
}
func fn7(){
	d1:=dog{}	// 值
	d2:=&dog{}	//指针

	var x Mover //x 既可以接收 值,也可以接收指针
	x = d1
	x.move()
	x = d2	// 对指针变量求值有语法糖,dog指针内部会自动求值 *dog
	x.move()
}
  • 然后看指针接收者实现接口
func (d *dog)move(){
	fmt.Println("dog moves..")
}

func fn7(){
	d1:=dog{}
	d2:=&dog{}

	var x Mover
	x = d1  //d1 是dog 类型, 这里会报错。x 不能接收 dog 类型
	x.move()

	x = d2 // d2 是 dog指针类型,x 能接收 dog指针类型
}

总结一下就是:

  • 值接收者的方法,能接收指针类型的参数
  • 指针接收者的方法,不能接收值类型的参数(没有语法糖)

1.6 类型与接口的关系

  • 一个类型可以实现多个接口。这些接口间彼此独立,不知道对方的实现
  • 多个类型实现能够同一接口。并且,一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现
func fn8(){
	d:= dryer{}
	h:= haier{d}

	var x WashingMachine
	x= h
	x.dry()	// WashingMachine并不直接实现 dry() 方法
	x.wash()
}
type WashingMachine interface {
	wash()
	dry()
}

type dryer struct{}

func (d dryer) dry() {
	fmt.Println("drying...")
}

type haier struct {
	dryer
}

func (h haier) wash() {
	fmt.Println("hair wash...")
}

1.7 接口嵌套

接口嵌套可以创造出新的接口。
demo略

1.8 空接口

空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
空接口类型的变量可以存储任意类型的变量.

空接口有啥作用?
1、 空接口作为函数的参数(空接口实现可以接收任意类型的的函数参数)

// 空接口能接收所有类型的参数
func show(a interface{}){
	fmt.Printf("type:%T value:%v \n", a,a)
}

2、空接口作为map的值 (使用空接口实现可以保存任意值的字典)

func fn9(){
	// 空接口能接收任意类型的 map 值
	var m =make(map[string]interface{})
	m["name"]="amy"
	m["age"]=10
	m["female"]=true
	fmt.Println(m)
}

1.9 接口值类型推断

  • 空接口可以存储任意值,所以空接口在go中的使用十分广泛
    Java中也有类型推断。空接口的使用,和java中的 instance关键字类似。
func fn11() {
	var w interface{}  // w 是一个接口值
	w = "amy" 
	//利用空接口进行类型推断。需要记住这种语法的返回参数,第一个是值,第二个是推断结果
	v, ok := w.(string)	
	if ok{
		fmt.Println(v)
	}else {
		fmt.Println("failed")
	}
}

1.10 使用接口值时Go做了啥

接口值 = 具体类型(动态类型)+ 具体类型的值(动态值)组成。在为接口值赋不同的具体类型的值时,动态类型和动态值会变化
在这里插入图片描述

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值