[TOC]
4 自定义类型&结构体定义
结构体默认是值类型
结构体是自定义类型的一种,在学结构体之前我们需要了解一下自定义类型
面向对象三大思想:
- 封装:在 go 里面实现封装就是用的结构体,将一堆属性不同的数据放到一起
- 继承:在其他代码段中可继承父代码的内容
- 多态:go 中没有比较好的多态示例
4.1 自定义类型
在 GO 里面是支持自定义类型的,语法如下:
type TypeName struct
// 在 go 里面通过 type 关键字定义我们的自定义类型
// TypeName 类型名字
// Formatter 为当前类型的格式
在 go 里面可以对当前已经存在的类型去定义,也可以是当前函数的一个签名
4.1.1 自定义基础类型
范例:
package main
import "fmt"
// 定义类型,Counter 定义的现有类型为 int
type Counter int
func main() {
// 定义 ints 变量类型为 Counter 赋值 1
var ints Counter = 1
fmt.Printf("type = %T \nvalue = %v\n", ints, ints)
}
输出:
[14:44:11 root@go day4]#go run type.go
type = main.Counter # 类型为 Counter
value = 1 # 值为 1
自定义类型,类型转换进行操作
当然也可以将自定义的类型强转为它对应的类型进行操作
package main
import "fmt"
// 定义类型,Counter 定义的现有类型为 int
type Counter int
func main() {
// ints 变量类型为 Counter 并赋值 1
var ints Counter = 1
var nums int = 10
// 这里强行将 nums 从 int 类型转为了 Counter
fmt.Printf("type = %T \nvalue = %v\n", ints, ints+Counter(nums))
}
输出:
[14:44:17 root@go day4]#go run type.go
type = main.Counter
value = 11
4.1.2 自定义 map 类型
定义一个 map 类型,用于存放 user 的信息
package main
import "fmt"
// 自定义类型 User 定义的现有类型为 map
// 这种 User 就是一个用户类型
type User map[string]string
func main() {
// 初始化和 map 的初始化一样的
// 可以用 var 也可以像这种短声明
user := make(User)
user["name"] = "zz"
user["age"] = "25"
fmt.Printf("type = %T \nvalue = %v\n", user, user)
}
输出:
[15:00:30 root@go day4]#go run type.go
type = main.User # 类型为 User
value = map[age:25 name:zz] # 值是 map 因为 User 的底层用了map
4.1.3 自定义 func 类型
package main
import "fmt"
// 自定义 Callback 定义的现有类型为 func() 函数
type Callback func() error
func main() {
// 定义一个 callbacks 为 map 类型,k 为 string value 为 Callback{}
callbacks := map[string]Callback{}
// 加入一个 callbacks["add"] 的 k 值为 func() error
callbacks["add"] = func() error {
fmt.Println("add")
return nil
}
// 调用 callbacks
callbacks["add"]()
}
输出:
[15:18:26 root@go day4]#go run type.go
add
给函数传参范例
package main
import "fmt"
type Callback func(string) error
func main() {
callback := map[string]Callback{}
callback["add"] = func(s string) error {
fmt.Println(s)
return nil
}
// 这里直接传递 dad 参数
callback["add"]("dad")
}
输出
[14:25:10 root@go day1]#go run main.go
dad
4.2 alias 别名
别名只是一个数据类型的别名 ,不能添加方法的。在工作中用的不多
其实就和 Linux 种起的一个别名一样的效果
package main
import "fmt"
// 定义别名在使用 type 关键字的时候加了 = ,那么就是在定义别名
type Counter = int
func main() {
var counters Counter = 10
fmt.Printf("type=%T\nvalue=%v\n", counters, counters)
}
输出:
[15:48:24 root@go day4]#go run alias.go
type=int # 通过输出可以看到是一个 int 类型,由于使用了别名所以最终还是使用的原数据类型
value=10
4.3 结构体
结构体是由一系列属性组成的复合数据类型,每个属性都具有名称、类型和值,结构体将属性组合在一起进行由程序进行处理
4.3.1 定义声明结构体
定义:
结构体定义使用 struct 标识,需要指定其包含的属性(名和类型),在定义结构体时可以为结构体指定结构体名(命名结构体),用于后续声明结构体变量使用
声明:
声明结构体变量只需要定义变量类型为结构体名,变量中的每个属性被初始化为对应类型的零值。也可声明结构体指针变量,此时变量被初始化为 nil
package main
import (
"fmt"
"time"
)
// 这就是定义了一个结构体,里面的对应的各个属性的类型都不一样
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
// 声明结构体
var user User
// 零值是由各个元素的零值组成的一个结构体的变量
fmt.Printf("type = %T\n默认值 = %#v\n", user, user)
}
输出:
[16:03:11 root@go day4]#go run struct.go
type = main.User
默认值 = main.User{id:0, name:"", addr:"", tel:"", birthday:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}}
# 通过输出我们可以看到他的默认 0 值其实是各个元素对应的零值组成的结构体
4.3.2 初始化赋值
使用结构体创建的变量叫做对应结构体的实例或者对象
字面量初始化有三种方法:
方法一:
按照结构体元素顺序进行赋值,而且每个元素之间的值必须传递
package main
import (
"fmt"
"time"
)
// 这就是定义了一个结构体,里面的对应的各个属性的类型都不一样
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
var user User
// 按照 user 结构体元素的顺序进行定义初始化
// 这种字面量(必须严格按照结构体属性顺序定义字面量)
// 每个属性都必须指定对应的值
user = User{1, "zz", "北京", "xx", time.Now()}
fmt.Printf("type = %T\n默认值 = %v\n", user, user)
}
输出:
[16:17:34 root@go day4]#go run struct.go
type = main.User
默认值 = main.User{id:1, name:"zz", addr:"北京", tel:"xx", birthday:time.Time{wall:0xc0283c3ac5b567dd, ext:131217, loc:(*time.Location)(0x55e540)}}
方法二:
按照属性名定义字面量,这种定义方式可以不用考虑结构体的属性顺序,也可以不给属性进行定义,没有字面量的属性值为默认值
package main
import (
"fmt"
"time"
)
// 这就是定义了一个结构体,里面的对应的各个属性的类型都不一样
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
var user User
// 按照结构体中的属性名称进行定义字面量
user = User{
name: "zz",
id: 12,
tel: "1232",
}
fmt.Printf("%v", user)
}
输出:
[16:22:08 root@go day4]#go run struct.go
{12 zz 1232 {0 0 <nil>}}
方法三:
通过短声明定义
package main
import (
"fmt"
"time"
)
// 这就是定义了一个结构体,里面的对应的各个属性的类型都不一样
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
user := User{
id: 12,
name: "zz",
}
fmt.Println(user)
}
输出:
[16:36:19 root@go day4]#go run struct.go
{12 zz {0 0 <nil>}}
4.3.3 结构体的属性访问&赋值
访问结构体单个元素:
我们可以通过下面的方式查看单个属性的值,结构体单个元素访问方法 结构体实体变量.结构体元素
package main
import (
"fmt"
"time"
)
// 这就是定义了一个结构体,里面的对应的各个属性的类型都不一样
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
var user User
user = User{
name: "zz",
id: 12,
tel: "1232",
}
// 单独访问 user 中的 name 和 id 两个属性
fmt.Println(user.name, user.id)
}
输出:
# 通过输出就可以看到 name 和 id 已经访问到了
[16:30:05 root@go day4]#go run struct.go
zz 12
修改单个结构体属性:
package main
import (
"fmt"
"time"
)
// 这就是定义了一个结构体,里面的对应的各个属性的类型都不一样
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
var user User
user = User{
name: "zz",
id: 12,
tel: "1232",
}
fmt.Println(user.name, user.id)
// 通过 结构体实体变量.属性 = 修改的值
user.name = "qqqq"
fmt.Println(user.name, user.id)
}
输出:
[16:35:09 root@go day4]#go run struct.go
修改前 zz 12
修改后 qqqq 12
4.4 结构体指针类型
结构体嵌入(命名&匿名)类型也可以为结构体指针
4.4.1 定义指针类型
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
// 通过 * 定义 User 为指针类型
var user *User
// 输出他的类型和他所有的值
fmt.Printf("%T,%#v\n", user, user)
}
输出:
[22:23:38 root@go day4]#go run pstruct.go
*main.User,(*main.User)(nil)
# 通过输出可以看到这是一个 指针类型默认值为 nil
4.4.2 指针类型初始化
默认不赋值初始化
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
// 通过 * 定义 User 为指针类型
// 直接字面量直接取地址进行初始化赋值
// & 取地址
var user *User = &User{}
fmt.Printf("%T,%#v\n", user, user)
}
输出:
[22:23:44 root@go day4]#go run pstruct.go
*main.User,&main.User{id:0, name:"", addr:"", tel:"", birthday:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}}
# *main.User 类型为指针类型
# &main.User{id:0, name:"", addr:"", tel:"", birthday:time.Time{wall:0x0, ext:0, loc:(*time.Location)(nil)}}
# &main.User 这里 & 表示取地址
进行赋值操作
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
// 通过 * 定义 User 为指针类型
// 直接字面量直接取地址进行初始化赋值
// & 取地址
// 这里赋值 id 12, name 为 zz
var user *User = &User{id: 12, name: "zz"}
// 也可以写成这样进行赋值 var user = &User{}
fmt.Printf("%T,%#v\n", user, user)
}
短声明并赋值操作
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
user := *&User{
id: 123,
name: "zzda",
}
// 如果这里不用 & 符号取地址的话默认就是 user 结构体类型
fmt.Printf("%T %v", &user, &user)
}
输出
[22:36:50 root@go day4]#go run pstruct.go
*main.User &{123 zzda {0 0 <nil>}}
4.4.3 New 函数初始化结构体指针
new
函数就是用来初始化结构体指针的
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
// new 函数初始化
user := new(User)
fmt.Printf("%T", user)
}
输出:
[22:48:28 root@go day4]#go run pstruct.go
*main.User
# 通过输出会发现就是一个指针类型
4.4.4 指针类型属性访问和修改操作
我们通过赋值了一个直接类型的结构体给 user 变量
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
func main() {
// new 函数初始化
user := new(User)
// 进行赋值
user.id = 123
// 对 user 指针类型的变量进行修改操作
user.id = 1444
fmt.Printf("%T %v", user, user.id)
}
输出:
[22:53:49 root@go day4]#go run pstruct.go
*main.User 1444
4.5 工厂函数
Go 语言中常定义 New + 结构体名命名的函数用于创建对应的结构体值对象或指针对象
构造函数一般就是用来创建类对应的实例
范例如下:
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
// 定义一个工厂函数 NewUser ,形参就是 user 结构体对应的属性类型,返回值为一个 User 结构体
func NewUser(id int, name, addr, tel string, birthday time.Time) User {
return User{id, name, addr, tel, birthday}
}
func main() {
// 定义 user 变量类型为 NewUser 函数类型,并且进行传参赋值
user := NewUser(1, "zzz", "北京", "177xxx", time.Now())
fmt.Printf("%T\n%v\n", user, user)
}
输出:
[23:10:36 root@go day4]#go run pstruct.go
main.User
{1 zzz 北京 177xxx {13846409812968821277 79159 0x55e540}}
# 通过输出 user 是一个结构体
# 而且还有值
当然返回的结构体也可以是指针类型
package main
import (
"fmt"
"time"
)
type User struct {
id int
name string
addr string
tel string
birthday time.Time
}
// 定义一个工厂函数 NewUser ,形参就是 user 结构体对应的属性类型,返回值为一个 User 结构体
func NewUser(id int, name, addr, tel string, birthday time.Time) *User {
return &User{id, name, addr, tel, birthday}
}
func main() {
user := NewUser(1, "zzz", "北京", "177xxx", time.Now())
fmt.Printf("%T\n%v\n", user, user)
}
通过输出可以看到是一个指针类型
工厂函数值的修改
可以通过结构体指针对象的点操作直接对对象的属性值进行访问和修改
4.6 结构体原理图
user 结构体在赋值成 u 的时候会在内存中重新生成一个内存空间,并且 u 这个结构体会将 user 的属性重新复制一份。
这个时候我们对 u 这个结构体进行修改或赋值的话肯定不会影响 user 结构体里面的内容。因为这个时候他们是两块不同的内存空间
前提是在 user 结构体中没有所谓的 指针、地址、切片、映射 类型,这两个结构体在相互定义的时候就不会有任何影响。如果说有 指针、地址、切片、映射 那他们两个在相互赋值的时候可能会有影响
4.7 匿名结构体
在定义变量时将类型指定为结构体的结构,此时叫匿名结构体。匿名结构体常用于初始化一次结构体变量的场景,例如项目配置
匿名结构体定义的变量只能定义一次
匿名结构体一般不使用指针
4.7.1 匿名结构体定义
package main
import "fmt"
func main() {
// 匿名结构定义不用 type 关键字
// 直接定义 user 变量,类型为 struct 由 id name addr tel 这 4 个属性组成的
var user struct {
id int
name string
addr string
tel string
}
fmt.Printf("类型:%T\n默认值:%#v\n", user, user)
}
输出:
[00:19:52 root@go day4]#go run anonyme.go
类型:struct { id int; name string; addr string; tel string }
默认值:struct { id int; name string; addr string; tel string }{id:0, name:"", addr:"", tel:""}
# 默认值就是每个属性类型的 0 值组成的
4.7.2 结构体修改&访问
package main
import "fmt"
func main() {
// 匿名结构定义不用 type 关键字
// 直接定义 user 变量,类型为 struct 由 id name addr tel 这 4 个属性组成的
var user struct {
id int
name string
addr string
tel string
}
user.id = 12
user.name = "zz"
user.id = 999
// 这里直接访问他的 id 和 name 属性
fmt.Printf("user id = %v\nuser name = %v", user.id, user.name)
}
输出:
[00:25:57 root@go day4]#go run anonyme.go
user id = 999
user name = zz
4.7.3 匿名结构体初始化赋值
通过 var
关键字赋值
package main
import "fmt"
func main() {
// 定义匿名结构体
var user struct {
id int
name string
addr string
tel string
}
// 匿名结构体初始化赋值
user = struct {
id int
name string
addr string
tel string
}{
id: 123,
name: "匿名结构体赋值",
addr: "北京",
tel: "177xxx",
} // 结构体类型
fmt.Println(user)
fmt.Println("访问单个元素 name = ", user.name)
}
输出:
[00:35:47 root@go day4]#go run anonyme.go
{123 匿名结构体赋值 北京 177xxx}
访问单个元素 name = 匿名结构体赋值
var
定义的时候直接赋值
package main
import "fmt"
func main() {
// 匿名结构体初始化的时候并赋值
var u2 struct {
id int
name string
} = struct {
id int
name string
}{
id: 1,
name: "sadas",
}
fmt.Println(u2)
}
输出:
[00:41:45 root@go day4]#go run anonyme.go
{1 sadas}
通过 :=
短声明赋值
package main
import "fmt"
func main() {
// 匿名结构体初始化赋值
user := struct {
id int
name string
addr string
tel string
}{
id: 123,
name: "短声明赋值",
addr: "短声明",
tel: "177xxx",
} // 结构体类型
fmt.Println(user)
fmt.Println("访问单个元素 name = ", user.name)
}
输出:
[00:36:12 root@go day4]#go run anonyme.go
{123 短声明赋值 短声明 177xxx}
访问单个元素 name = 短声明赋值
5 结构体组合(继承)
所谓继承结构体就是在当前已有的结构之上进行扩展并使用。
组合其实可以理解为里面有个属性定义为了另外一个结构体类型
5.1 结构体命名组合
组合结构体范例
现在我们想把这个 user 的地址详情区分一下
package main
import (
"fmt"
)
type Addr struct {
province string // 省份
street string // 街道
no string // 门牌号
}
// 定义 Tel 电话,电话也分国际和国内电话
type Tel struct {
prefix string
number string
}
// 组合
type User struct {
id int
name string
addr Addr // 继承 Addr 结构体,这就是组合的一种方式
tel Tel // 继承 tel 结构体
}
func main() {
// 通过 var 定义
var user = User{
id: 9527,
name: "七七",
addr: Addr{
province: "北京",
street: "龙景苑",
no: "602"},
tel: Tel{
prefix: "001",
number: "177499",
},
}
fmt.Println(user)
// 短声明定义 user 实体变量接收 User 结构体并且初始化
user1 := User{
id: 12,
name: "zgy",
addr: Addr{
province: "重庆",
street: "祥和街道",
no: "17-3",
},
tel: Tel{prefix: "023",
number: "47678276",
},
}
fmt.Println(user1)
// 访问获取单个属性元素
fmt.Println("获取单个省份:",user1.addr.province)
// 修改结构体继承的单个元素
user1.addr.province = "成都"
fmt.Println("修改后的值", user1.addr.province)
}
输出:
[21:58:05 root@go day4]#go run combind.go
{9527 七七 {北京 龙景苑 602} {001 177499}}
{12 zgy {重庆 祥和街道 17-3} {023 47678276}}
获取单个省份: 重庆
修改后的值 成都
5.1.1 结构体组合指针类型进行初始化
当我们组合的结构体是一个指针类型时,在定义的时候需要取出地址
package main
import (
"fmt"
)
type Addr struct {
province string // 省份
street string // 街道
no string // 门牌号
}
// 定义 Tel 电话,电话也分国际和国内电话
type Tel struct {
prefix string
number string
}
// 组合
type User struct {
id int
name string
addr *Addr // 继承 Addr 结构体,并且是一个指针类型
tel *Tel // 继承 tel 结构体,指针类型
}
func main() {
// 短声明定义 user 实体变量接收 User 结构体并且初始化
user1 := User{
id: 12,
name: "zgy",
addr: &Addr{
province: "重庆",
street: "祥和街道",
no: "17-3",
},
tel: &Tel{prefix: "023",
number: "47678276",
},
}
fmt.Println(user1)
// 访问继承结构体的单个属性
fmt.Println(user1.addr.province)
// 修改继承结构体单个属性
user1.addr.province = "成都"
fmt.Println(user1.addr.province)
}
输出:
[22:05:39 root@go day4]#go run combind.go
{12 zgy 0xc00006e150 0xc00002c040} # 由于是指针类型所以取出的是地址
重庆
成都
5.1.2 结构体属性指针类型的好处
结构体指针类型的好处就是当我们修改了对应的值,其他继承了同样结构体的变量属性值也会修改
package main
import (
"fmt"
)
type Addr struct {
province string // 省份
street string // 街道
no string // 门牌号
}
// 定义 Tel 电话,电话也分国际和国内电话
type Tel struct {
prefix string
number string
}
// 组合
type User struct {
id int
name string
addr *Addr // 继承 Addr 结构体,并且是一个指针类型
tel *Tel // 继承 tel 结构体,指针类型
}
func main() {
// 短声明定义 user 实体变量接收 User 结构体并且初始化
user1 := User{
id: 12,
name: "zgy",
addr: &Addr{
province: "重庆",
street: "祥和街道",
no: "17-3",
},
tel: &Tel{prefix: "023",
number: "47678276",
},
}
fmt.Println(user1)
// 这将 user1 这个变量的值赋值给了 u2
u2 := user1
// 修改 u2.addr.province 的值
u2.addr.province = "修改"
// 输出 user1.addr.province 元素
fmt.Println(user1.addr.province)
}
输出:
[22:12:44 root@go day4]#go run combind.go
{12 zgy 0xc00010e150 0xc000130000}
修改
# 通过输出 user1.addr.province 值,我们发现已经改为了 修改,因为底层都是在对同一个内存地址进行操作
5.2 结构体匿名组合
在 go 里面可以使用匿名组合,在定义属性的时候指定义类型并不指定名称
package main
import "fmt"
type Addr struct {
province string
street string
}
type User struct {
id int
name string
Addr // 继承 Addr 结构体,它默认使用的就是属性名就是 继承结构体的名称
}
func main() {
// 定义并赋值继承结构体
user := User{
Addr: Addr{province: "重庆", street: "新河街"},
}
//匿名结构体继承访问可以直接访问继承过来的 结构体属性
fmt.Println(user.province)
// 匿名结构体继承的属性修改
user.province = "修改为北京"
fmt.Println(user.province)
}
输出:
[22:28:41 root@go day4]#go run anonymestruct.go
重庆
修改为北京
5.2.1 结构体和继承结构体有共同属性名的时候操作
但是当我们的 User 结构体里面也有属性是 province 的时候我们就需要指定匿名结构体的名称来进行操作
package main
import "fmt"
type Addr struct {
province string
street string
}
type User struct {
id int
name string
Addr
province string
}
func main() {
// 定义并赋值
user := User{
Addr: Addr{province: "重庆", street: "新河街"},
}
// 当结构体和继承的结构体有共同属性名的时候分别赋值操作
// 修改结构体自身的 province 属性值,和访问
user.province = "直接指定属性名称即可"
fmt.Println("user.province:",user.province)
// 修改被继承结构体时需要指定继承结构体的名称和需要修改的属性名
user.Addr.province = "指定继承结构体名称在加上属性名称"
// 访问时也需要指定全路径
fmt.Println("user.Addr.province:",user.Addr.province)
}
输出:
[22:49:28 root@go day4]#go run anonymestruct.go
user.province: 直接指定属性名称即可
user.Addr.province: 指定继承结构体名称在加上属性名称
5.2.2 匿名组合指针
匿名组合继承的时候也可以使用指针
package main
import "fmt"
type Addr struct {
province string
}
type User struct {
id int
name string
*Addr // 定义指针类型的匿名结构体
}
func main() {
user := User{
Addr: &Addr{province: "北京"}, // 访问的时候通过 & 符取地址
}
// 指针继承结构体修改
user.Addr.province = "北京"
// 访问
fmt.Printf("%#v\n", user.Addr.province)
}
输出:
[23:03:56 root@go day4]#go run anonymestruct.go
"北京"
6 结构体可见性(封装)
封装的好处就是能够实现安全
结构体首字母大写则包外可见(公开的),否者仅包内可访问(内部的)
结构体属性名首字母大写包外可见(公开的),否者仅包内可访问(内部的)
组合:
- 结构体名首字母大写,属性名大写:结构体可在包外使用,且访问其大写的属性名
- 结构体名首字母大写,属性名小写:结构体可在包外使用,且不能访问其小写的属性名
- 结构体名首字母小写,属性名大写:结构体只能在包内使用,属性访问在结构体嵌入时 由被嵌入结构体(外层)决定,被嵌入结构体名首字母大写时属性名包外可见,否者只能在包内使用
- 结构体名首字母小写,属性名小写:结构体只能在包内使用
目录结构:
创建了一个 models 文件夹,里面有个 user.go
文件代码如下
package models
// User1 结构体可以被调用
type User1 struct {
private string // 不能被调用首字母小写
Public string // 能被调用首字母大写
}
// user2 结构体不能被调用
type user struct {
private string
Public string
}
然后到 main.go
中进行调用
package main
import (
"fmt"
"visibility/models"
)
func main() {
// 调用 user1
user1 := models.User1{}
// 调用 Public 属性
user1.Public = "公开的"
fmt.Printf("%T\n%v\n", user1, user1)
}
输出:
只能访问首字母大写的属性,首字母小写的属性依旧不能被访问
[21:22:31 root@go visibility]#go run main.go
models.User1
{ 公开的}
6.1 工厂函数实现对私有结构体的访问
虽然首字母小写的结构体在外面不能直接访问,但是我们可以通过工厂函数实现访问
在 user.go 程序代码如下
package models
// 私有化结构体 user2
type user2 struct {
private string
Public string
}
// 编写工厂函数,返回值为 user2
func NewUser() *user2 {
return &user2{}
}
在 main.go 中代码
package main
import (
"fmt"
"visibility/models"
)
func main() {
user2 := models.NewUser()
fmt.Printf("%T\n", user2)
user2.Public = "访问"
fmt.Printf("%#v\n", user2.Public)
}
输出:
只能访问首字母大写的属性,首字母小写的属性依旧不能被访问
[21:38:42 root@go visibility]#go run main.go
*models.user2
"访问"
7 方法
基本介绍
在某些情况下,我们需要声明定义一些方法。比如 person 结构体,除了一些字段外(年龄,姓名…),person 结构体还有一些行为比如:可以说话、跑步、学习等等。这时候就需要使用到方法才能完成
注意:
方法和函数很相似,或者说使用方法基本都相同的,但是两个概念还是有所区别的
Golang 中方法是作用在指定的数据类型上的(也就是说这个方法是给某个数据类型绑定的),因此自定义类型,都可以有方法(就是通过 type 来定义声明的都可以有方法),而不仅仅是 struct
重点:
- 方法是和数据类型相互绑定的
- 自定义类型都可以有方法、而非 struct
方法是为特定类型定义的,只能由该类型调用的函数。
对于结构体里面的属性如果是小写的话再包外是不可见的。这个时候就需要使用到我们的方法
7.1 定义方法
语法:
func (变量名 type) methodName(参数列表) (返回值列表) {
方法体
return 返回值
}
语法说明:
- 变量名 type:表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型。type 可以是结构体,也可以是其他的自定义类型
- 变量名:就是 type 类型的变量(实例),比如:person 结构体的一个变量(实例)
- methodName:方法的名字
- 参数列表:表示方法输入,类似于函数传参
- 返回值列表:表示为了实现某一功能代码块,如果说这个方法有返回,这个返回值就可以写到这里
- 方法主体:表示为了实现某一功能代码块
- return:返回值,如果有返回值列表那一定会有 return
7.2 跨包调用方法&指针接收者
当我们写了私有的结构体的时候,或者是私有的结构体属性的时候可以通过工厂函数的访问
范例如下
先在 models 包中编写代码如下:
package models
import "fmt"
type user struct {
id int
name string
age int
}
// 编写 info 方法打印 user 结构体全部属性信息, *user 指针接收者
func (user *user) Info() {
fmt.Println(user.id, user.name, user.age)
}
// 编写 AddAge 方法累加 user.age ,*user 指针接收者
func (user *user) AddAge() {
user.age++
}
// GetName 方法返回 user.name , *user 指针接收者
func (user *user) GetName() string {
return user.name
}
// 定义工厂函数 NewUser 并传入 id name age 参数,返回一个 user 结构体的指针类型
func NewUser(id int, name string, age int) *user {
return &user{
id: id,
name: name,
age: age,
}
}
然后再 main.go 程序中调用
package main
import (
"fmt"
"visibility/models"
)
func main() {
// 定义 user 变量接收 models.NewUser() 函s数的返回值,也就是结构体 user 类型
user := models.NewUser(1, "zz", 22)
fmt.Printf("%T\n", user)
fmt.Println(*user)
// 直接调用 AddAge() 和 Info() 方法
user.AddAge()
user.Info()
// 输出 user.GetName() 方法的返回值
fmt.Println(user.GetName())
}
输出:
通过输出我们会发现已经修改了 age 从 22 改为了 23
[22:24:14 root@go visibility]#go run main.go
*models.user
{1 zz 22}
1 zz 23
zz
7.3 方法注意事项
由于结构体是值拷贝类型,当我们在 test() 方法中赋值了其他的值,是互不影响的如下
package main
import (
"fmt"
)
type Person struct {
Name string
}
// test() 是这个方法的名字,而且这里 Person 接收者是一个值类型
func (a Person) test() {
// 对 a.Name 赋值为 方法
a.Name = "方法"
fmt.Println("test()", a.Name)
}
func main() {
// 定义 p 变量类型为 person
var p Person
p.Name = "zhang"
// 调用 test 方法
p.test()
// 输出主函数中的 p.Name
fmt.Println("main()", p.Name)
}
输出:
[22:33:39 root@go visibility]#go run main.go
test() 方法
main() zhang
但是如果将方法中结构体传入的是指针类型就可以修改,因为指针是在对同一块内存地址做操作
范例如下
package main
import (
"fmt"
)
type Person struct {
Name string
}
// test() 是这个方法的名字,传入的是指针类型
func (a *Person) test() {
// 对 a.Name 赋值为 方法
a.Name = "方法"
fmt.Println("test()", a.Name)
}
func main() {
// 定义 p 变量类型为 person
var p Person
p.Name = "zhang"
// 调用 test 方法,从而实现了 a.Name = "方法"
p.test()
// 输出主函数中的 p.Name
fmt.Println("main()", p.Name)
}
输出:
通过输出我们会发现修改
[22:48:43 root@go visibility]#go run main.go
test() 方法
main() 方法 # 修改为方法
7.4 方法注意事项和细节讨论
7.4.1 细节一
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式。
7.4.2 细节二
如果程序员在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理(通常情况下更多是和结构体指针进行绑定,因为这样做能够提高传递的效率)
因为如果是和结构体本身进行绑定就是值拷贝,如果是和结构体指针进行绑定那就是引用传递
范例代码:
package main
import (
"fmt"
)
type Circle struct {
redius float64
}
// 为了提高效率,通常我们方法和结构体的指针类型绑定,也就是通过 * 指针运算符号,这时就是传入 c 的地址
func (c *Circle) area() float64 {
// 因为 c 是指针,因此我们标准的访问其字段的方式是 (*c).redius
// 但是 go 设计者在编译器底层做了优化也可以写成下面代码
return 3.14 * c.redius * c.redius
}
func main() {
var c Circle
c.redius = 4.0
// 但是 golang 的设计者感觉这样写太麻烦了,所以 go 的作者在底层做了优化如下代码:
// 编译器底层做了优化 (&).area() 等价于 c.area()
// 因为编译器会自动的加上 & 取地址符号
res := c.area()
fmt.Println("圆的面积 = ",res)
}
输出:
在方法中修改结构体中字段的值:
在方法中修改结构体中字段的值、也会修改到 main 函数中的对应字段的值
package main
import (
"fmt"
)
type Circle struct {
redius float64
}
// 为了提高效率,通常我们方法和结构体的指针类型绑定,也就是通过 * 取值符号,这时就是传入 c 的地址,这个 c 其实就直接指向了 main 函数中的 c
func (c *Circle) area() float64 {
// 这里将 c.redius 改为了 10,而不在是只拷贝了则是引用传递
c.redius = 10
return 3.14 * c.redius * c.redius
}
func main() {
var c Circle
c.redius = 4.0
// 因为这里调用了 c.area() 方法,由于时地址传递,所以会将 main 函数中的 c.redius = 4.0 修改
res := c.area()
// 因为是传入的地址,所以在 area() 方法中改变 c.redius 的值也会直接改变 main 函数中 c.redius的值
fmt.Println("圆的面积 = ",res)
fmt.Println("main()中的 redius = ",c.redius)
}
输出:
通过输出我们可以看到 main 函数中的 redius 值已经被修改了
原理图讲解:
- 执行
var c Circle
就会在内存中开辟一块空间,main 栈 - 执行
c.redius = 7.0
c 是一个结构体指向了数据空间,然后在这个数据空间中有个 redius 字段 值为 7.0 - 执行
c.area2()
会开辟一个独立数据空间,area2 栈 - 执行
(c *Circle) area2()
此时此刻当area2()
方法发现 c.redius = 7.0 是一个地址,就会将该地址拷贝到 area2 中的 *Circle 中 - 执行
c.radius = 10
将 redius 字段值改为了 10 - 执行
return 3.14 * c.redius * c.redius
这时候的 c 就是从 main 栈中取得,也就是这个结构体指向的数据空间得值
7.4.3 细节三
Golang 中的方法作用在指定的数据类型上(即:和指定的数据类型绑定),因此自定义类型都可以有方法,而不仅仅是 struct(结构体),比如 int、float32 等都可以有自己的方法
范例代码:
package main
import (
"fmt"
)
/*
Golang 中的方法作用在指定得数据类型上(即:和指定得数据类型绑定),
因此自定义类型都可以有方法,而不仅仅是 struct(结构体),比如 int、float32 等都可以有自己的方法
*/
// 给 int 做了一个自定义,相当于 integer 就是 int 的别名
type integer int
// 给他绑定方法,这个方法叫做 print()
func (i integer) print() {
fmt.Println("i =",i)
}
func main() {
// 定义一个 maini 变量,类型为 integer 类型 值为 10
var maini integer = 10
// 通过 maini 变量调用 print 方法,将 maini 值输出
maini.print()
}
输出:
编写一个方法,可以改变 i 的值范例:
package main
import (
"fmt"
)
// 给 int 做了一个自定义,相当于 integer 就是 int 的别名
type integer int
// 请编写一个方法,可以改变 i 的值,这里传入的是 maini 的指针运行符,及 *integer
func (i *integer) change() {
// 取值运算符,将 main 函数中的 maini = 10 传递过来,由于是取地址,所以此处修改了 i 的值也就是间接改了 maini 的值
*i = *i +1
}
func main() {
// 定义一个 maini 变量,类型为 integer 类型 值为 10
var maini integer = 10
// 将 maini 的地址传递给让 change 方法中
maini.change()
fmt.Println("修改后的 change 方法中的 i =",maini)
}
输出:
7.4.4 细节四
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问。方法首字母大写,可以在本包外的其他包访问。
7.4.5 细节五
如果一个类型实现了 String() 这个方法,那么 fmt.Println 默认会调用这个变量的 String() 进行输出
范例代码:
package main
import (
"fmt"
)
// 定义 student 结构体
type Student struct {
Name string
Age int
}
// 给他 student 实现 String() 方法
func (stu *Student) String() string {
str := fmt.Sprintf("Name=[%v] Age=[%v]",stu.Name,stu.Age)
return str
}
func main() {
// 先定义一个 mainStu 变量,类型为 Student 结构体
mainStu := Student{
Name : "tom",
Age : 20,
}
// 这里没有调用方法,而是直接通过 fmt.Println 默认会调用这个变量的 String() 方法进行输出
// 这里通过 & 取地址符将 mainStu 传递给 String() 方法
fmt.Println(&mainStu)
}
输出:
通过输出我们发现他直接调用了 String() 方法
7.5 组合结构体方法调用
若结构体匿名嵌入带有方法的结构体时,则在外部结构体可以调用嵌入结构体的方法,并且在调用时只有嵌入的字段会传递给嵌入结构体方法的接收者。
当被嵌入结构体与嵌入结构体具有相同名称的方法时,则使用对象方法名调用被嵌入结构体方法。若想要调用嵌入结构体方法,则使用对象嵌入结构体名方法
7.5.1 命名组合范例
范例如下:
package main
import "fmt"
// 定义 addr 结构体
type Addr struct {
province string
street string
no string
}
// 定义 setprovince 方法并且是指针类型
func (addr *Addr) SetProvince(province string) {
addr.province = province
}
// 命名组合
type User struct {
id int
name string
addr Addr // 命名组合
}
func main() {
user := User{}
// 在命名组合的时候必须写上他的命名如 addr. 通过全路径访问
user.addr.SetProvince("龙锦苑")
fmt.Println(user.addr.province)
}
输出:
[15:03:35 root@go day4]#go run methodcombind.go
龙锦苑
7.5.2 匿名组合
package main
import "fmt"
// 定义 addr 结构体
type Addr struct {
province string
street string
no string
}
// 定义 setprovince 方法并且是指针类型
func (addr *Addr) SetProvince(province string) {
addr.province = province
}
type User struct {
id int
name string
Addr // 匿名组合
}
func main() {
user := User{}
// 匿名组合就可以直接通过方法调用即可
user.SetProvince("龙锦苑")
fmt.Println(user.province)
// 或者通过全路径的方式修改
user.Addr.SetProvince("东二区")
fmt.Println(user.Addr.province)
}
输出
[15:05:25 root@go day4]#go run methodcombind.go
龙锦苑
东二区
7.5.2.1 当结构体自身方法名和匿名组合方法重复时
如果原结构体和匿名组合的结构体有共同的方法名,想调用匿名组合的结构体,则需要写全路径,否则它默认调用的是自己的方法
package main
import "fmt"
// 定义 addr 结构体
type Addr struct {
province string
street string
no string
}
// 定义 setprovince 方法并且是指针类型
func (addr *Addr) SetProvince(province string) {
addr.province = province
}
type User struct {
id int
name string
Addr // 命名组合
province string
}
// 给 user 绑定方法 SetProvince()
func (user *User) SetProvince(province string) {
user.province = province
fmt.Println("user setprovince", user.province)
}
func main() {
user := User{}
// 默认会调用自己的方法
user.SetProvince("龙锦苑")
// 如果调用别的匿名结构体方法,就必须得写全路径
user.Addr.SetProvince("东二区")
fmt.Println(user.Addr.province)
}
输出:
[15:16:43 root@go day4]#go run methodcombind.go
user setprovince 龙锦苑
东二区
7.5.2.2 当多个匿名结构体方法名重复时
如果我们有多个匿名结构体中的方法名重复时,默认调用会报错,必须写想要访问得某个方法即可,而且必须是全路径
错误范例:
package main
import "fmt"
// 定义 addr 结构体
type Addr struct {
province string
street string
no string
}
// 定义 Addr 结构体的 setprovince 方法
func (addr *Addr) SetProvince(province string) {
addr.province = province
}
type Tel struct {
}
// 定义 Tel 结构体的 setprovince 方法
func (tel *Tel) SetProvince(name string) {
fmt.Println("tel tel tel", name)
}
type User struct {
id int
name string
Addr // 匿名结构体继承
Tel // 匿名结构体继承
}
func main() {
user := User{}
user.SetProvince("龙锦苑")
user.SetProvince("东二区")
fmt.Println(user.province)
}
输出报错:
[15:25:42 root@go day4]#go run methodcombind.go
# command-line-arguments
./methodcombind.go:35:6: ambiguous selector user.SetProvince
./methodcombind.go:38:6: ambiguous selector user.SetProvince
# 不明确的选择器user.SetProvince
正确范例:
通过全路径来进行访问
package main
import "fmt"
// 定义 addr 结构体
type Addr struct {
province string
street string
no string
}
// 定义 Addr 结构体的 setprovince 方法
func (addr *Addr) SetProvince(province string) {
addr.province = province
}
type Tel struct {
}
// 定义 Tel 结构体的 setprovince 方法
func (tel *Tel) SetProvince(name string) {
fmt.Println("tel tel tel", name)
}
type User struct {
id int
name string
Addr // 匿名结构体继承
Tel // 匿名结构体继承
}
func main() {
user := User{}
// 这是访问 Tel 的 setprovince 方法
user.Tel.SetProvince("龙锦苑")
// 这是访问 user 的 setprovince 方法
user.Addr.SetProvince("东二区")
fmt.Println(user.Addr.province)
}
输出
[15:29:51 root@go day4]#go run methodcombind.go
tel tel tel 龙锦苑
东二区
8 方法值/方法表达式 (了解即可)
方法也可以赋值给变量,存储在数组、切片、映射中,也可作为参数传递给函数或作为函数返回值进行返回方法有两种,一种是使用对象/对象指针调用的(方法值),另一种是有类型/类型指针调用的(方法表达式)
8.1 对象调用(方法值)
范例:
package main
import "fmt"
type User struct {
}
func (user *User) Pfunc() {
fmt.Println("Pfunc")
}
func (user User) Vfunc() {
fmt.Println("Vfunc")
}
func main() {
// 对象调用 =》 方法值,也就是说某个方法得值类型
var user User
// 这里将 user.Pfunc 方法赋值给 methodValue 变量
methodValue := user.Pfunc
fmt.Printf("%T\n", methodValue)
methodValue() // 调用 methodValue
// 调用值接收方法
methodValue2 := user.Vfunc
fmt.Printf("%T\n", methodValue2)
methodValue2()
// 将 user 变量地址取出来传递给 u2
u2 := &user
methodValue11 := u2.Pfunc // u2.Pfunc 方法赋值给 methodValue11
fmt.Printf("%T\n", methodValue11)
methodValue11()
methodValue12 := u2.Vfunc // u2.Vfunc 方法赋值给 methodValue12
fmt.Printf("%T\n", methodValue12)
methodValue12()
}
输出:
[15:52:27 root@go day4]#go run methodobj.go
func()
Pfunc
func()
Vfunc
func()
Pfunc
func()
Vfunc
8.2 结构体调用(方法表达式)
通过结构体调用得时候,如果这个结构体绑定是指针类型则需要在调用的时候通过指针调用
定义接收者方式:
值接收者
- 自动生成指针接收者
- 值接收者
指针接收者
- 只有指针接收者方法
package main
import "fmt"
type User struct {
}
func (user *User) Pfunc() {
fmt.Println("Pfunc")
}
func (user User) Vfunc() {
fmt.Println("Vfunc")
}
func main() {
var user User
methodValue := User.Vfunc
fmt.Printf("%T\n", methodValue)
// 取值得时候一定要通过结构体来取
methodValue(User{})
methodValue(user)
// 值接收者,也会自动生成指针接收者
methodValue1 := (*User).Vfunc
fmt.Printf("%T\n", methodValue1)
// 指针取值就必须要先通过 & 取地址在进行取值
methodValue1(&user)
methodValue1(&User{})
// 指针调用的时候必须通过指针来获取
methodValue2 := (*User).Pfunc
fmt.Printf("%T\n", methodValue2)
// 指针取值就必须要先通过 & 取地址在进行取值
methodValue2(&user)
methodValue2(&User{})
}
输出:
[16:22:44 root@go day4]#go run methodobj.go
func(main.User)
Vfunc
Vfunc
func(*main.User)
Vfunc
Vfunc
func(*main.User)
Pfunc
Pfunc
8.3 细节
我们在进行值类型绑定方法的时候,由于不是对同一块内存空间做修改,所以会发现是没有值的
package main
import "fmt"
type User struct {
id int
name string
}
func (u *User) pfunc() {
fmt.Printf("pfunc: id = %d name = %s\n", u.id, u.name)
}
func (u User) vfunc() {
fmt.Printf("vfunc: id = %d name = %s\n", u.id, u.name)
}
func main() {
user := User{}
value := user.pfunc // 指针接收者 => 对同一块内存空间做修改
value2 := user.vfunc // 值接收者 => user 被赋值 给 value2
user.id = 10
user.name = "hahahha"
value()
value2() // 并不是对同一块内存空间做修改
}
输出:
root@consul-3:~/go/src/2022/day1# go run main.go
pfunc: id = 10 name = hahahha
vfunc: id = 0 name = # 通过输出可以看到值类型并不会输出对应的值,因为不共享同一块内存空间
9 结构体结合函数使用
在工作中我们会遇见下面这种问题,假如说有个函数需要传递很多参数而且还想要每个参数都有默认值。
为了解决这个问题可以通过结构体来进行优化如下代码
package main
import "fmt"
// 这里我将 test 函数需要的形参封装成结构体
type testArgs struct {
a, b int
c, d string
}
// 形参默认值通过工厂函数实现
func NewTest() *testArgs {
return &testArgs{
a: 1,
b: 2,
c: "c",
d: "d",
}
}
func testv1(args *testArgs) {
fmt.Printf("%T\n", args)
fmt.Println(args.a)
fmt.Println(args.b)
fmt.Println(args.c)
fmt.Println(args.d)
}
func main() {
testv1(NewTest())
}
执行
root@consul-3:~/go/src/2022/day1# go run main.go
*main.testArgs
1
2
c
d