什么是结构体
结构体就是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语言的面向对象方法中,是用结构体来实现上面的要求,即用结构体实现封装,用封装实现高内聚、低耦合。
评论区