文章目录
一. 自定义类型 type
1. 自定义类型
在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type
关键字来定义自定义类型
。
自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct
定义。例如:
//将MyInt定义为int类型
type MyInt int
通过Type关键字的定义,MyInt
就是一种新的类型,它具有int
的特性。
2. 类型别名
类型别名是Go1.9
版本添加的新功能。
类型别名规定:TypeAlias只是Type的别名
,本质上TypeAlias与Type是同一个类型。最终指向的是本人
type TypeAlias = Type
rune和byte
就是类型别名
type byte = uint8
type rune = int32
3. 类型定义和类型别名的区别
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a : %T\n", a) //type of a:main.NewInt
fmt.Printf("type of b : %T\n", b) //type of b:int
}
//结果
type of a : main.NewInt
type of b : int
a的类型是main.NewInt,表示main包下定义的NewInt类型。
b的类型是int。MyInt类型只会在代码中存在
,编译完成时并不会有MyInt类型。
也就是自定义
类型最终输出的类型为自定义
的类型,而别名
最终为原始
类型
二. 结构体
什么是结构体?
数组与切片,只能存储同一类型的变量。
若要存储多个类型的变量,就需要用到结构体
,它是将多个任意类型的变量组合在一起的聚合数据类型
。
每个变量都成为该结构体的成员变量。
可以理解为 Go语言 的结构体struct
和其他语言的class
有相等的地位,但是Go语言放弃大量面向对象的特性,所有的Go语言类型除了指针类型外,都可以有自己的方法,提高了可扩展性
。
Go 语言没有 class 类的概念,只有 struct
结构体的概念,因此也没有继承
1. 定义结构体
声明结构体
type 结构体名 struct {
属性名 属性类型
属性名 属性类型
...
}
比如我要定义一个可以存储个人资料名为 Profile
的结构体,可以这么写
type Profile struct {
name string
age int
gender string
mother *Profile // 指针
father *Profile // 指针
}
若相邻的属性(字段)是相同类型,可以合并写在一起
type Profile struct {
name,gender string
age int
mother *Profile // 指针
father *Profile // 指针
}
或者
var user struct{Name string; Age int}
结构体初始化
最常用的方式
type person struct {
name string
city string
age int8
}
func main() {
var p1 person
p1.name = "qcq.cn"
p1.city = "北京"
p1.age = 18
fmt.Printf("p1=%v\n", p1) //p1={qcq.cn 北京 18}
fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"qcq.cn", city:"北京", age:18}
}
1. 直接定义
通过结构体可以定义一个组合字面量
,有几规则
。
规则一:当最后一个字段和结果不在同一行时,
不可省略。
p1 := person{
name: "qcq.cn",
city: "北京",
age: 18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"qcq.cn", city:"北京", age:18}
反之,不在同一行,就可以省略。
xm := Profile{
name: "小明",
age: 18,
gender: "male"}
规则二:字段名要么全写
,要么全不写
,不能有的写,有的不写。
例如下面这种写法是会报 mixture of field:value and value initializers
错误的
xm := Profile{
name: "小明",
18,
"male",
}
2 . 使用值的列表初始化
字段名
全都 不写
p8 := &person{
"qcq.cn",
"北京",
18,
}
fmt.Printf("p8=%#v\n", p8) //p8=&main.person{name:"qcq.cn", city:"北京", age:18}
需要注意:
1.必须初始化结构体的
所有字段
。
2.初始值的填充顺序
必须与字段在结构体中的声明顺序一致。
3.该方式不能和键值初始化方式混用
。
3. 结构体指针初始化
全部写
对结构体指针
进行键值对初始化,例如:
p6 := &person{
name: "pprof.cn",
city: "北京",
age: 18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}
规则三:初始化结构体,并不一定要所有字段都赋值
,未被赋值的字段,会自动赋值为其类型的零值
。
必须指定字段名
才可以赋值部分字段。
xm := Profile{name: "小明"}
fmt.Println(xm.age)
// output: 0
否则会报错
# command-line-arguments
./demo.go:19:51: too few values in Profile literal
4. 结构体初始化
type person struct {
name string
city string
age int8
}
func main() {
var p4 person
fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}
5. 结构体内存布局
可以看出结构体再内存中是连续的
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
面试题
type student struct {
name string
age int
}
func main() {
//这个字典的,值为结构体student
m := make(map[string]*student)
stus := []student{
{name: "qcq", age: 22},
{name: "测试", age: 30},
{name: "博客", age: 38},
}
//map无序,所以结果有多种可能
for _, stu := range stus {
//相当于qcq=qcq,22
//&stu是将 该地址的内容赋值
m[stu.name] = &stu
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
二. 结构体方法
1. 如何定义一个结构体方法
不能在结构体内定义方法
,
可以用组合函数的方式来定义结构体方法。
type Profile struct {
name string
age int
gender string
mother *Profile // 指针
father *Profile // 指针
}
//方法
func (person Profile) FmtProfile() {
fmt.Printf("名字:%s\n", person.name)
fmt.Printf("年龄:%d\n", person.age)
fmt.Printf("性别:%s\n", person.gender)
}
其中FmtProfile 是方法名,而(person Profile) :表示将 FmtProfile 方法与 Profile 的实例绑定。
我们把 Profile 称为方法的接收者,而 person 表示实例本身
,在方法内可以使用 person.属性名 的方法来访问实例属性。
package main
import "fmt"
// 定义一个名为Profile 的结构体
type Profile struct {
name string
age int
gender string
mother *Profile // 指针
father *Profile // 指针
}
// 定义一个与 Profile 的绑定的方法
func (person Profile) FmtProfile() {
fmt.Printf("名字:%s\n", person.name)
fmt.Printf("年龄:%d\n", person.age)
fmt.Printf("性别:%s\n", person.gender)
}
func main() {
// 实例化
myself := Profile{name: "小明", age: 24, gender: "male"}
// 调用函数
myself.FmtProfile()
}
//输出如下
名字:小明
年龄:24
性别:male
//Person 结构体
type Person struct {
name string
age int8
}
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
return &Person{
name: name,
age: age,
}
}
//Dream Person做梦的方法
func (p Person) Dream() {
fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}
func main() {
p1 := NewPerson("测试", 25)
p1.Dream()
}
2. 方法的参数传递方式(重点)
当你想要在方法内改变实例的属性
的时候,必须使用指针做为方法的接收者
。
package main
import "fmt"
// 声明一个 Profile 的结构体
type Profile struct {
name string
age int
gender string
mother *Profile // 指针
father *Profile // 指针
}
// 重点在于这个星号: *, 说明已经有一个struct对象实例化了
//是 再次基础上做修改
func (person *Profile) increase_age() {
person.age += 1
}
func main() {
myself := Profile{name: "小明", age: 24, gender: "male"}
fmt.Printf("当前年龄:%d\n", myself.age)
myself.increase_age()
fmt.Printf("当前年龄:%d", myself.age)
}
//结果
当前年龄:24
当前年龄:25
可以看到在方法内部对 age 的修改已经生效。你可以尝试去掉 *,使用值做为方法接收者,看看age是否会发生改变(答案是:不会改变
)
至此,我们知道了两种定义方法的方式:
-
以
值
做为方法接收者 -
以
指针
做为方法接收者
那我们如何进行选择呢?
- 直接使用
指针
做为方法的接收者。
- 你需要在方法内部改变结构体内容的时候
- 出于性能的问题,当结构体过大的时候(相当于,组合)
- 有些情况下,以值或指针做为接收者都可以,但是考虑到代码一致性,
建议
都使用指针做为接收者。
指针类型的接收者
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
p.age = newAge
}
调用该方法:
func main() {
p1 := NewPerson("测试", 25)
fmt.Println(p1.age) // 25
p1.SetAge(30)
fmt.Println(p1.age) // 30
}
4. 结构体实现 “继承”
Go 语言本身并不支持继承。
可以使用组合的方法,实现类似继承的效果。
组合:比如一台电脑,是由机身外壳,主板,CPU,内存等零部件组合在一起,最后才有了我们用的电脑。
在 Go 语言中,把一个结构体嵌入到另一个结构体的方法,称之为组合。
现在这里有一个表示公司(company)的结构体,还有一个表示公司职员(staff)的结构体。
type company struct {
companyName string
companyAddr string
}
type staff struct {
name string
age int
gender string
position string
}
若要将公司信息与公司职员关联起来,一般都会想到将 company 结构体的内容照抄到 staff 里。
借鉴继承的思想,我们可以将公司的属性都“继承”过来。
但是在 Go 中没有类的概念,只有组合,
可以将 company 这个 结构体嵌入
到 staff 中,做为 staff 的一个匿名字段,staff 就直接拥有了 company 的所有属性了。
type staff struct {
name string
age int
gender string
position string
company // 匿名字段
// cpmpay compay // 嵌套
}
验证一下。
package main
import "fmt"
type company struct {
companyName string
companyAddr string
}
type staff struct {
name string
age int
gender string
position string
company
}
func main() {
myCom := company{
companyName: "Tencent",
companyAddr: "北京市",
}
staffInfo := staff{
name: "小明",
age: 28,
gender: "男",
position: "云计算工程师",
company: myCom,
}
/*
user1 := User{
Name: "pprof",
Gender: "女",
Address: Address{
Province: "陕西",
City: "西安",
},
}
*/
fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.companyName)
fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.company.companyName)
}
结果,可见staffInfo.companyName 和 staffInfo.company.companyName 的效果是一样的。
小明 在 Tencent 工作
小明 在 Tencent 工作
嵌套类型
嵌套结构体内部可能存在相同的字段名
。这个时候为了避免歧义需要指定具体的内嵌结构体的字段
//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
user2.Address.Province = "陕西" //通过匿名结构体.字段名访问
user2.City = "西安" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
继承
使用结构体也可以实现其他编程语言中面向对象的继承
//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
5. 内部方法与外部方法(大小写)
函数名的首字母大小写非常重要,它被来实现控制对方法的访问权限。
-
当方法的首字母为
大写
时,这个方法对于所有包都是Public,其他包可以随意调用
-
当方法的首字母为
小写
时,这个方法是Private,其他包是无法访问
。
6. 三种实例化方法
第一种:正常实例化
func main() {
xm := Profile{
name: "小明",
age: 18,
gender: "male",
}
}
第二种:使用 new
func main() {
xm := new(Profile)
// 等价于: var xm *Profile = new(Profile)
fmt.Println(xm)
// output: &{ 0 }
xm.name = "iswbm" // 或者 (*xm).name = "iswbm"
xm.age = 18 // 或者 (*xm).age = 18
xm.gender = "male" // 或者 (*xm).gender = "male"
fmt.Println(xm)
//output: &{iswbm 18 male}
}
第三种:使用 &
func main() {
var xm *Profile = &Profile{}
fmt.Println(xm)
// output: &{ 0 }
xm.name = "iswbm" // 或者 (*xm).name = "iswbm"
xm.age = 18 // 或者 (*xm).age = 18
xm.gender = "male" // 或者 (*xm).gender = "male"
fmt.Println(xm)
//output: &{iswbm 18 male}
}
7. 选择器的冷知识
从一个结构体实例对象中获取字段的值,通常都是使用 . 这个操作符,该操作符叫做 选择器
。
当你对象是结构体对象的指针时,你想要获取字段属性
时,按照常规理解应该这么做
type Profile struct {
Name string
}
func main() {
//这里直接将p1定义为指针类型
p1 := &Profile{"iswbm"}
fmt.Println((*p1).Name) // output: iswbm
}
有一个更简洁的做法,可以直接省去 * 取值的操作
,选择器 . 会直接解引用,示例如下
type Profile struct {
Name string
}
func main() {
p1 := &Profile{"iswbm"}
fmt.Println(p1.Name) // output: iswbm
}
8. 结构体与JSON序列化
JSON
是一种轻量级的数据交换格式
。
JSON键值对是用来保存JS对象
的一种方式,键/值
对组合中的键名写在前面并用双引号""
包裹,使用冒号:
分隔,然后紧接着值;多个键值之间使用英文,
分隔。
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
9. 结构体标签(Tag)
Tag是结构体的元信息
,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号
包裹起来,具体的格式如下:
key1:"value1" key2:"value2"
结构体标签由一个或多个
键值对组成。键与值使用冒号
分隔,值用双引号
括起来。键值对之间使用一个空格分隔
。
注意事项: 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。
例如我们为Student结构体的每个字段定义json序列化
时使用的Tag:
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
func main() {
s1 := Student{
ID: 1,
Gender: "女",
name: "pprof",
}
data, err := json.Marshal(s1)
if err != nil {
fmt.Println("json marshal failed!")
return
}
fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"女"}
}
10. map类型的结构体
package main
import "fmt"
type student struct {
id int
name string
age int
}
func main() {
ce := make(map[int]student)
ce[1] = student{1, "xiaolizi", 22}
ce[2] = student{2, "wang", 23}
fmt.Println(ce)
//如何删除
delete(ce, 2)
fmt.Println(ce)
}
实现map有序输出
(面试经常问到)
package main
import (
"fmt"
"sort"
)
func main() {
map1 := make(map[int]string, 5)
map1[1] = "www.topgoer.com"
map1[2] = "rpc.topgoer.com"
map1[0] = "xiaohuang"
map1[5] = "qcqly"
map1[3] = "xiaohong"
sli := []int{}
for k, _ := range map1 {
sli = append(sli, k)
}
sort.Ints(sli)
for i := 0; i < len(map1); i++ {
fmt.Println(map1[sli[i]])
}
}
思考
package main
import "fmt"
type student struct {
id int
name string
age int
}
func demo(ce []student) {
//切片是引用传递,是可以改变值的
ce[1].age = 999
// ce = append(ce, student{3, "xiaowang", 56})
// return ce
}
func main() {
var ce []student //定义一个切片类型的结构体
ce = []student{
student{1, "xiaoming", 22},
student{2, "xiaozhang", 33},
}
fmt.Println(ce)
demo(ce)
fmt.Println(ce)
}
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体
。
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "qcq.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
}
1. 创建指针类型结构体
我们还可以通过使用new
关键字对结构体进行实例化,得到的是结构体的地址
。 格式如下:
var p2 = new(person)
fmt.Printf("%T\n", p2) //*main.person
fmt.Printf("p2=%#v\n", p2)
//结果
//p2=&main.person{name:"", city:"", age:0}
从打印的结果中我们可以看出p2是一个结构体指针
。
在Go语言中支持对结构体指针直接使用
.来访问结构体的成员。
var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}
2. 取结构体的地址实例化
使用&
对结构体进行取地址操作相当于**对该结构体类型进行了一次new实例化
**操作。
p3 := &person{}
fmt.Printf("%T\n", p3) //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "qcq"
p3.age = 21
p3.city = "西安"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"qcq", city:"西安", age:21}
p3.name = "qcq"
其实在底层是(*p3).name = "qcq"
,
这是Go语言帮我们实现的语法糖。