目 录CONTENT

文章目录

Go学习系列12-结构体

cplinux98
2022-11-19 / 0 评论 / 0 点赞 / 788 阅读 / 2,906 字 / 正在检测是否收录...

什么是结构体

结构体就是go语言中的类似python类的一种结构

结构体不是表示哪一个事物,而是泛指事物这一类

结构体的格式

type 结构体名称 struct {
    字段名称 类型
}
  • 结构体中的成员变量首字母如果大写,代表可以导出
    • 这个导出是包级别的,不是类型级别
  • 结构体可以包含可导出和不可导出成员变量
  • 结构体不能将它自己定义为成员变量,但可以把它自己的指针类型定义为成员变量
type Man struct{
    Name string
    Age int
    son *Man
}

在go语言中没有继承的概念,继承提供了扩展性,但是牺牲了间接性,这种方式是通过侵入式实现的。GO语言鼓励使用组合的方式,这是非侵入式的,不会破坏类型封装或让类型之间耦合更紧密

我们可以把每个结构体都制作好之后,嵌入结构体中,这样就可以使用它的成员变量和方法了,组合的方式十分灵活,就像积木一样可以随意组合

type Man struct {
    Name string
    certificate
}

type certificate struct {
    Title string
    GetTime time.Time
}

接口类型也可以组合,这样可以扩展接口定义的行为。

使用结构体

创建一个结构体变量

wang := Man{"小王", 25, certificate{}, nil}
fmt.Printf("%s", wang.Name)
fmt.println((&wang).Name)
  • 结构体变量的成员变量通过点号可以访问到
  • 结构体变量通过成员变量的地址,再通过指针也可以访问到

也可以使用下面的方式,类似于python函数中的按名称传入一样创建结构体实例

li := Man{
    Name: "老李",
    Age: "18",
    certificate: certificate{},
    son: nil
}

或者通过指针的方式,创建一个结构体变量

zhang := &Man{
    Name: "老张",
    Age: "88",
    certificate: certificate{},
    son: nil
}

上面的等价于

zhang := new(Man)
*zhang = Man{
    Name: "老张",
    Age: "88",
    certificate: certificate{},
    son: nil
}

匿名成员与结构体嵌套

上面我们在Man结构体内嵌套了一个certificate结构体,Go语言可以定义不带名称的结构体成员,只要指定类型就可以,这种结构体成员叫做匿名成员。这个结构体成员的类型必须是一个命名类型或者命名类型的指针。

对匿名成员可以直接访问变量

	li := Man{
		Name:        "李",
		Age:         18,
		certificate: certificate{},
		son:         nil,
	}
	li.Title = "CCIE"
	fmt.Println

但是,不能在一个结构体内定义两个一样的匿名成员,否则会引起冲突,因为匿名成员有隐式的名字。

如果一个结构体的所有成员都可以比较,那么该结构体就可以比较。

结构体内部可以嵌套一个结构体,但在嵌套时,被嵌套的结构体需要经过初始化

type s1 struct{
    Name string
}
type s2 struct{
    *s1 // 可以是指针,也可以是结构体
}

匿名的结构体

当结构体只需要调用一次,就可以使用匿名结构体,无需通过type关键词定义,直接使用

cp := struct {
    Name string
    Age int
}{
    Name: "cp",
    Age: 24
}

结构体与json

json是开发中交互数据的主要格式之一,尤其是在前后端开发时,更是必不可少

结构体与json互转

package main

import (
	"encoding/json"
	"fmt"
)

type Chef struct {
	Name string
	Age  int
}

func struct2json() {
	c := Chef{
		Name: "李",
		Age:  24,
	}
	marshal, err := json.Marshal(&c)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Println(string(marshal))
}

func json2struct() {
	var cc Chef
	var s string
	s = `{"Name":"李","Age":24}`
	err := json.Unmarshal([]byte(s), &cc)
	if err != nil {
		fmt.Println(err)
	}
	fmt.Printf("%#v\n", cc)
	fmt.Println(cc.Name)
	fmt.Println(cc.Age)
}

func main() {
	struct2json()
	//{"Name":"李","Age":24}
	json2struct()
	//main.Chef{Name:"李", Age:24}
	//李
	//24

}

使用注解修改转为json时的名称

因为我们给前端返回时,需要的是小写开头,而go里面可导出的需要大写,这时就可以使用注解修改json化时的名称了

func struct2json2() {
	type User struct {
		Name     string `json:"name"`
		Age      int    `json:"age"`
		Language string `json:"lang"`
	}
	li := User{
		Name:     "李",
		Age:      24,
		Language: "golang",
	}
	jsonStr, err := json.Marshal(&li)
	if err == nil {
		fmt.Println(string(jsonStr))
	} else {
		fmt.Println(err)
	}
}

func main() {
	struct2json2()
	//{"name":"李","age":24,"lang":"golang"}
}

方法

方法可以看作某种特定类型的函数,方法的声明和普通函数的声明类似,只是在函数名称前面多了一个参数。这个参数是一个类型,可以把这个方法绑定在对应的类型上。

func (接收者 接收者类型) 方法名称 (参数列表...)(返回值列表{
    方法体
    return
}

参数列表和返回值列表可以为空。如果返回值列表为空,那么return可以省略

示例:

type S1 struct{
    Name string
    Age int
}

// 给S1绑定一个方法
func (s S1) SayHi(name string) string {
    return s.Name + ": 向" + name + "问好"
}

  • 这里面接收者没有固定的名字(python里面的self,java里面的this),需要开发者自己命名
  • 接收者经常被使用,所以接收者的名字应简短且与类型名称保持一致,例如S1中的s
  • 调用方法时,接收者在类型名称的前面,接收者是某一类型的变量

调用示例:

package main

import "fmt"

type S1 struct {
	Name string
	Age  int
}

func (s S1) SayHi(name string) string {
	return s.Name + ": 向" + name + "问好"
}

func main() {
	li := S1{
		Name: "李",
		Age:  24,
	}
	result := li.SayHi("老王") // 调用方法
	fmt.Println(result)

}

指针接收者方法

在Go语言中,值传递时会复制一个变量,在遇到下面两种情况时,应该使用指针类型作为方法的接收者。

  • 在调用方法时,需要更新变量
  • 类型的成员很多,占用内存很大,使用指针类型可以节省内存使用率
package main

import "fmt"

type S1 struct {
	Name string
	Age  int
}

func (s *S1) SayHi(name string) string {
	return s.Name + ": 向" + name + "问好"
}

当往结构体上组合方法时,接收者的类型是一个指针类型时,它的方法会变成这样:

(*S1).SayHi

而不是

*(S1.SayHi)

指针接收者调用方法的三种方式

  • li := &S1{
        Name: "li",
        Age: 24,
    }
    result := li.SayHi("老王")
    fmt.Println(result)
    
  • li := S1{
        Name: "li",
        Age: 24,
    }
    liPoint := &li // 接收li的指针
    result :- liPoint.SayHi("老王")
    fmt.Println(result)
    
  • li := S1{
        Name: "李",
        Age: 24,
    }
    result := (&li).SayHi("老王")
    fmt.Println(result)
    

实参接收者type与*type

如果实参接收者c是S1类型的变量,但是方法要求一个*S1接收者,那么Go语言编译器会对变量进行&c的隐式转换

package main

import "fmt"

type S1 struct {
	Name string
	Age  int
}

func (s S1) SayHi(name string) string {
	return s.Name + ": 向" + name + "问好"
}

func (s *S1) SayHi2(name string) string {
	return s.Name + ": 向" + name + "问好"
}

func main(){
    li := S1{  // 此时li变量是一个S1类型
        Name: "li",
        Age: 24,
    }
    result := li.SayHi2("老王")
    fmt.Println("%s", result)
    fmt.Printf("%T", li) 
}

如果接收者类型是*S1,那么S1.SayHi的调用也是合法的,因为在从地址中获取S1的值时,编译器会自动插入一个隐式的*操作符号

package main

import "fmt"

type S1 struct {
	Name string
	Age  int
}

func (s S1) SayHi(name string) string {
	return s.Name + ": 向" + name + "问好"
}

func (s *S1) SayHi2(name string) string {
	return s.Name + ": 向" + name + "问好"
}

func main() {
	li := &S1{ // 此时li 变量是一个 *main.S1 类型
		Name: "李",
		Age:  24,
	}
	result := li.SayHi("老王")
	fmt.Println(result)
	fmt.Printf("%T", li)
}

总结

  • 如果实参接收者(实例)是Type类型的变量,而形参接收者(定义方法时)是*Type,则编译器会隐式地获取变量的地址
  • 如果实参接收者(实例)是*Type,而形参接收者(定义方法时)是Type类型的变量,则编译器会隐式地调用接收者,获取实际的值
  • 不能对一个不能取址(&)的Type接收者参数(实例)调用*Type方法,因为无法获取变量的地址

当接收者实参(实例)是nil时,比如切片或者字典等,因为他们的初始值就是nil,此时我们需要先对使用接收者和nil做比较,再进行其他业务逻辑处理。

值方法与指针方法的区别

值方法

func (s S1) SayHi(name string) string {
	return s.Name + ": 向" + name + "问好"
}

指针方法

func (s *S1) SayHi2(name string) string {
	return s.Name + ": 向" + name + "问好"
}

区别在于:

  • 值方法的接收者是该方法所属类型值的副本(实例的副本),再方法体内执行的操作大多是对该副本进行修改,基本上不会改变原值,但引用类型(字典、切片等)除外
  • 指针类型的接收者是该方法所属类型的指针(实例本身),再方法体内执行的操作,是对指针指向的值进操作,可以修改原值

在自定义的结构体方法集合中仅仅包含它的值方法,在该结构体的指针类型的方法集合中包括了所有的值方法。

Go语言的结构体类型方法只能调用值方法,但是Go语言的编译器会适当地进行自动转译,进而也能调用它的指针方法。

type S1 struct {
	Name string
	Age  int
}

func (s *S1) SayHi2(name string) string {
	return s.Name + ": 向" + name + "问好"
}

func main() {
	li := &S1{ // 此时li 变量是一个 *main.S1 类型
		Name: "李",
		Age:  24,
	}
	result := li.SayHi("老王")
	fmt.Println(result)
	fmt.Printf("%T", li)
}

方法与表达式

在调用方法时,必须有接收者(实例),但是在表达式中:Type.func 或者 (*Type).func,Type时类型,与前面介绍的函数变量类似

liSayHi := li.SayHi
r := liSayHi("老王")
fmt.println(r)

总结

结构体是Go语言中面向对象编程的重要组成部分。就相当于python中的类。

  • 封装
    • 封装的意义在于保护或防止代码(数据)被我们或其他调用者无意中破坏
    • 保护成员属性,不让结构体以外的程序直接访问或修改
    • 隐藏方法的细节

封装的原则是:高内聚,低耦合

内聚:是指一个模块各部分之间的关联程度

耦合:是值各模块之间的关联程度

封装可以隐藏对象的属性和实现细节,仅仅对外公开访问方法,并控制访问级别,在Go语言的面向对象方法中,是用结构体来实现上面的要求,即用结构体实现封装,用封装实现高内聚、低耦合。

0

评论区