1. 自定义类型和类型别名
1.1. 自定义类型
Go语言中除了基本的数据类型之外,还可以自定义数据类型,最常用的是就是结构体,除此之外还可以基于基本数据类型来实现自定义类型,如: type newType type
。基于基本数据类型创造自定义类型的目的在于,部分场景中,需要对已有类型添加新的方法,此时需要使用到自定义类型。
package main
import "fmt"
type MyInt int32
func main() {
var a MyInt = 133
fmt.Printf("%T %#v %d\n", a, a, a) // main.MyInt 133 133, main包中MyInt类型
}
1.2. 类型别名
在Go中最常见的类型别名就是 rune 和 byte ,分别为 int32 和 uint8 的别名。别名只是在代码中用于更好的区分不同数据类型,一旦编译完毕后,就直接映射为原始的数据类型。
package main
import "fmt"
type testInt = uint64
func main() {
var (
a testInt = 65539
b rune = '渡'
c rune = '#'
)
fmt.Printf("a --> Type:%T Value:%v\n", a, a)
fmt.Printf("b --> Type:%T Value:%v\n", b, b)
fmt.Printf("c --> Type:%T Value:%v\n", c, c)
}
[root@heyingsheng 01-type]# go run 01.go
a --> Type:uint64 Value:65539
b --> Type:int32 Value:28193
c --> Type:int32 Value:35
# rune 和 byte 类型
[root@heyingsheng 01-type]# grep -E "rune|byte" /mnt/c/Program\ Files/Go/src/builtin/builtin.go | grep -v '//'
type byte = uint8
type rune = int32
1.3. 类型别名和自定义类型的区别
- 自定义类型编译完毕后,仍然是自定义类型;类型别名编译完毕后,编译完毕后变成原始的数据类型
- 类型别名是方便写代码时区分,自定义类型是方便扩展功能,如方法
2. 定义结构体
GoLang中支持面向对象编程,但是并不是通过Class方式来实现的,而是通过结构体(struct)来实现的,GoLang的面向对象思维的编程方式比较简单,去掉了传统OOP语言中的继承、方法重载、构造函数、析构函数以及this指针等特性。GoLang中仍然有面向对象编程的继承、封装和多态的特性,只是实现的方式不同而言。
2.1. 结构体的定义
2.1.1. 定义结构体
结构体的名称就是一种数据类型,其它内部的字段可以具有不同的数据类型,这样就实现了一个实例的不同属性。注意:
- 同一个包中结构体数据类型不能重名
- 同一个结构体中字段不能重名
- 不能指定字段的默认值
- 结构体默认是值类型,指针类型的结构体需要单独创建
type Person struct {
name string
gender string
age uint8
hobby []string
}
2.1.2. 字段简写
多个同类型的字段可以写在一行,格式:field1, field2, field3 Type
type service struct {
name, logDir, dataDir string
port []uint16
}
2.1.3. 匿名结构体
上述两种方式定义的结构体都有一个类型名称,可以被不同的实例所引用,参考 2.2. 结构体的初始化 。如果一个结构体只是使用一次,那么可以不定义结构体名称,直接完成定义。 ```go package main
func main() { var redis struct{ name string port uint16 dataDir string } redis.name = “Redis” redis.port = 6379 // 直接赋值 var nginx = struct{ name string port []uint16 }{“nginx”, []uint16{80,8080,8081}} }
<a name="WbAa5"></a>
#### 2.1.4. 结构体的匿名字段
匿名字段指没有声明字段名称,指声明了字段类型的结构体,这种结构体的字段名称与字段类型一致。因此同一个类型只能定义一个,否则就出现了结构体中字段名称冲突。 了解即可,**不建议使用!**
```go
type student struct {
string
uint8
}
var stu01 student
stu01.string = "张三"
2.2. 结构体的实例化
将结构体作为数据类型赋值给一个变量,称为结构体的实例化,实例化后的变量被称为(结构体的)实例、(结构体的)对象:
- 实例化有两大类:以定义变量的形式实例化结构体,使用构造函数来实例化
- 通过变量形式实例化有三种:
- 使用
var.field = value
格式直接对字段赋值,常用 - 使用 key/value 方式来赋值,与map 类型类似,常用
- 使用 value 直接赋值,这种方式要求按顺序对所有字段赋值
- 使用
- 字段的默认值为对应数据类型的零值
import ( “fmt” “strings” )
type Person struct { name string gender string age uint8 hobby []string }
func main() { // 结构体的默认值 var p0 Person fmt.Printf(“p0: %v %#v\n”, p0, p0) fmt.Println(strings.Repeat(“-“,30)) // 先实例化,再赋值,未赋值的使用字段零值 var p1 Person p1.name = “张三” p1.age = 18 p1.hobby = []string{“篮球”,”乒乓球”,”足够”} fmt.Printf(“p1: %v\n”, p1) fmt.Println(strings.Repeat(“-“,30)) // 使用key/value来实例化 p2 := Person{ name: “李四”, gender: “女”, age: 24, hobby: []string{“旅游”,”购物”}, } fmt.Printf(“p2: %v\n”, p2) fmt.Println(strings.Repeat(“-“,30)) // 直接使用value来实例化 p3 := Person{ “王五”, “男”, 35, []string{“游泳”,”钓鱼”}, } fmt.Printf(“p3: %v\n”, p3) }
[root@heyingsheng 02-struct]# go run 03.go
p0: { 0 []} main.Person{name:””, gender:””, age:0x0, hobby:[]string(nil)}
p1: {张三 18 [篮球 乒乓球 足够]}
p2: {李四 女 24 [旅游 购物]}
p3: {王五 男 35 [游泳 钓鱼]}
<a name="th3AP"></a>
#### 2.2.2. 结构体的指针
```go
package main
import "fmt"
type Service struct {
Name string
Port []uint16
}
func main() {
// 使用new定义结构体的指针类型的变量
sshd := new(Service)
sshd.Port = []uint16{22} //语法糖,是 (*sshd).port 的简写
sshd.Name = "SSHD"
fmt.Printf("sshd 类型:%T; 值:%v; 16进制:%p; 结构体的值:%v\n", sshd, sshd, sshd, *sshd)
// 直接使用结构体指针进行定义
nginx := &Service{}
nginx.Name = "Nginx"
nginx.Port = []uint16{80, 8080}
fmt.Printf("nginx 类型:%T; 值:%v; 16进制:%p; 结构体的值:%v\n", nginx, nginx, nginx, *nginx)
}
[root@heyingsheng 02-struct]# go run 05.go
sshd 类型:*main.Service; 值:&{SSHD [22]}; 16进制:0xc000066330; 结构体的值:{SSHD [22]}
nginx 类型:*main.Service; 值:&{Nginx [80 8080]}; 16进制:0xc0000663c0; 结构体的值:{Nginx [80 8080]}
2.2.3. 使用构造函数来实例化
构造函数类似于Python中 __init__()
函数,是为了更方便的完成结构体的实例化。需要注意的是:
- 构造函数名称约定俗成用new开头
- 构造函数分两类,返回结构体的构造函数和返回结构体指针的构造函数
- 返回结构体的构造函数,会将函数内生成的结构体赋值给接收变量,内存的开销较大
- 返回结构体指针的构造函数,只需要对返回的指针进行拷贝,内存的开销较小,推荐使用 ```go package main
import “fmt”
type service struct { name string port []uint16 dataDir string logDir string man string }
func NewService(name string, port []uint16, dataDir string, logDir string, man string) service { return service{ name: name, port: port, dataDir: dataDir, logDir: logDir, man: man, } }
func NewServicePointer(name string, port []uint16, dataDir string, logDir string, man string) *service { return &service{ name: name, port: port, dataDir: dataDir, logDir: logDir, man: man, } }
func main() { nginx := NewService(“Nginx”,[]uint16{80,8080},”/data/nginx/html”,”/data/nginx/logs”,””) sshd := NewServicePointer(“sshd”,[]uint16{22},””,”/var/log/“,””) fmt.Printf(“nginx —> value:%v; type:%T\n”, nginx, nginx) fmt.Printf(“sshd —> value:%v; type:%T ;dstvalue:%v\n”, sshd, sshd, *sshd) }
[root@heyingsheng day04]# go run 05-structfunc/main.go nginx —> value:{Nginx [80 8080] /data/nginx/html /data/nginx/logs }; type:main.service sshd —> value:&{sshd [22] /var/log/ }; type:*main.service ;dstvalue:{sshd [22] /var/log/ }
<a name="ycM2o"></a>
### 2.3. 结构体值的访问和修改
<a name="3pmqv"></a>
#### 2.3.1. 结构体的访问
```go
package main
import "fmt"
type service struct {
name string
port []uint16
dataDir string
logDir string
man string
}
func main() {
mysql := service{
name: "MySQL",
port: []uint16{3306},
dataDir: "/data/mysql/db",
logDir: "/data/mysql/logs",
man: "",
}
fmt.Println(mysql.name,mysql.dataDir)
}
2.3.2. 结构体数据修改
对于结构体的数据修改有两种方式:一个是通过 name.field = value
,一种是通过函数修改。
- 通过函数修改时,函数传参传的是实参的副本,因此在函数想要修改结构体的值,必须要要采用指针的方式。
- go语言中通过结构体指针操作结构体数据时,可以省略
*
,这是go的语法糖 ```go package main
import ( “fmt” “strings” )
type service struct { name string port []uint16 dataDir string logDir string man string }
func f0(s service) { fmt.Printf(“函数内结构体内存地址:%p\n”, &s) s.dataDir = “/data/mysql/data” }
func f1(s service) { //(s).dataDir = “/data/mysql/data” s.dataDir = “/data/mysql/data” // 这种写法是go的语法糖,本质就是上一行的写法 }
func main() { mysql := service{ name: “MySQL”, port: []uint16{3306}, dataDir: “/data/mysql/db”, logDir: “/data/mysql/logs”, man: “”, } mysql.dataDir = “/data/mysql/database” fmt.Println(mysql.name,mysql.dataDir) fmt.Println(strings.Repeat(“-“,30)) fmt.Printf(“实例化的mysql内存地址:%p\n”, &mysql) f0(mysql) fmt.Println(mysql.name,mysql.dataDir) fmt.Println(strings.Repeat(“-“,30)) f1(&mysql) fmt.Println(mysql.name,mysql.dataDir) }
[root@heyingsheng day04]# go run 03-accessstruct/main.go
MySQL /data/mysql/database
实例化的mysql内存地址:0xc00001a0c0 函数内结构体内存地址:0xc00001a120
MySQL /data/mysql/database
MySQL /data/mysql/data
<a name="dDoAj"></a>
#### 2.4. 嵌套结构体
<a name="VyNbU"></a>
#### 2.4.1. 嵌套结构体的定义
嵌套结构体类似于Json,用于存储复杂数据类型,比如存储Kubernetes中的资源配置清单,如下案例:
```go
package main
import "fmt"
// 定义k8s资源清单metadata字段
type metadata struct {
name string
namespace string
}
// 定义k8s资源清单顶层字段
type topInfo struct {
aipVersion string
kind string
metadata
}
// 定义pod
type pod struct {
info topInfo
spec struct{
containers []string // 仅做一个示范而已,不操作更加复杂结构
}
}
// 定义service
type service struct {
topInfo
spec struct{
clusterIP string
}
}
func main() {
var nginxPod = pod{
info: topInfo{
aipVersion: "v1",
kind: "Pod",
metadata: metadata{
name: "nginx",
namespace: "default",
},
},
spec: struct {
containers []string
}{[]string{"nginx"}},
}
fmt.Println(nginxPod.info.aipVersion, nginxPod.info.namespace,nginxPod.spec.containers)
nginxSVC := service{
topInfo: topInfo{
aipVersion:"v1",
kind:"Service",
metadata: metadata{
name: "nginx",
namespace: "default",
},
},
spec: struct {
clusterIP string
}{"192.168.0.112"},
}
fmt.Println(nginxSVC.aipVersion, nginxSVC.namespace,nginxSVC.spec.clusterIP)
}
[root@heyingsheng day04]# go run 08-nested/main.go
v1 default [nginx]
v1 default 192.168.0.112
通过上面的案例可以分析得到:
1. 实现结构体嵌套有两种方式:
(1) 将每一层的结构拆开定义(line5-14)。这种方式定义繁琐,赋值时方便(line32-39)
(2) 直接在一个结构体中,使用struct来定义嵌套的结构体(line18-20),定义方便,赋值繁琐(line54-56)
2. 嵌套结构体访问
(1) 如果嵌套的为匿名结构体(line13,line24),在查询值的时候可以省略中间字段,如nginxSVC.namespace
(2) 如果嵌套的为命名结构体(line17),在查询的时候就得写全路径,如nginxPod.info.aipVersion
(3) 当多个嵌套结构体中存在相同名称字段,则不适合使用匿名结构体
2.4.2. 结构体转json
使用 json.Marshal(name)
可以将结构体转换为字符切片,转为string后就是json字符串。需要注意的是结构体字段名称首字母需要大写,以下为简单演示。
package main
import (
"encoding/json"
"fmt"
)
type service struct {
// 只有字段名称首字母大写才能被其它包读取到,此时json字符串中的字段名都为大写字母开头
// 如果需要指定在json中该字段显示的名称,则需要使用tag指定
Name string `json:"name"`
Port []uint16 `json:"port"`
DataDir string `json:"data_dir"`
LogDir string `json:"log_dir"`
Man string `json:"man"`
}
func main() {
nginx := service{
Name: "Nginx",
Port: []uint16{80, 8080, 8081},
DataDir: "/data/nginx/www",
LogDir: "/data/nginx/logs",
Man: "",
}
ret, err := json.Marshal(nginx)
if err == nil {
fmt.Printf("type: %T; value:%v\n", string(ret), string(ret))
}
}
[root@heyingsheng day04]# go run 10-json/main.go
type: string; value:{"name":"Nginx","port":[80,8080,8081],"data_dir":"/data/nginx/www","log_dir":"/data/nginx/logs","man":""}
[root@heyingsheng day04]# echo '{"name":"Nginx","port":[80,8080,8081],"data_dir":"/data/nginx/www","log_dir":"/data/nginx/logs","man":""}'|python3 -m json.tool
{
"name": "Nginx",
"port": [
80,
8080,
8081
],
"data_dir": "/data/nginx/www",
"log_dir": "/data/nginx/logs",
"man": ""
}
2.4.3. json转结构体
json转结构体,需要先定义好接收json准换后的结构体变量,通过 json.Unmarshal([]byte, &name)
可以将字符切片转为结构体。需要注意的是:
json.Unmarshal()
接收的是字符切片和结构体变量的指针- 必须要先定义好结构体和结构体变量 ```go package main
import ( “encoding/json” “fmt” )
type service struct {
// 只有字段名称首字母大写才能被其它包读取到,此时json字符串中的字段名都为大写字母开头
// 如果需要指定在json中该字段显示的名称,则需要使用tag指定
Name string json:"name"
Port []uint16 json:"port"
DataDir string json:"data_dir"
LogDir string json:"log_dir"
Man string json:"man"
}
func main() {
var ret service
jsonStr := {"name":"MySQL","port":[3306],"data_dir":"/data/mysql/data","log_dir":"/data/mysql/logs","man":"db"}
err := json.Unmarshal([]byte(jsonStr), &ret)
if err == nil {
fmt.Printf(“type: %T; value:%v\n”, ret, ret)
}
}
[root@heyingsheng day04]# go run 10-json/main.go type: main.service; value:{MySQL [3306] /data/mysql/data /data/mysql/logs db}
<a name="ff5KI"></a>
### 2.5. 注意事项
<a name="pJCJI"></a>
#### 2.5.1. 结构体的修改
```go
package main
import "fmt"
type Person struct {
Name string
Age uint8
}
func f0() {
p1 := Person{Name: "Tom", Age: 18}
p2 := p1
p2.Name = "Stone" // 结构体是指针类型,因此p2是p1的副本,因此修改p2的属性不会影响p1
fmt.Println("f0:", p1)
fmt.Println("f0:", p2)
}
func f1() {
p1 := &Person{Name: "Tom~", Age: 18}
p2 := p1
p2.Name = "Stone~" // p1是结构体指针,因此p2中存储的是p1的地址,而p1的地址又指向了结构体实例
fmt.Println("f1:", *p1)
fmt.Println("f1:", *p2)
}
func main() {
f0()
f1()
}
[root@heyingsheng 13-notice]# go run 01.go
f0: {Tom 18}
f0: {Stone 18}
f1: {Stone~ 18}
f1: {Stone~ 18}
2.5.2. 内存地址的连续性
package main
import "fmt"
type S21 struct {
N1, N2 int8
N3, N4 int64
}
type S22 struct {
x, y int8
}
type S23 struct {
m, n S22
}
type S24 struct {
m, n *S22
}
func f21() {
// N1和N2是连续的,地址相差为1。N3和N4是连续的。N2和N3之所以中间有中断是为了方便寻址,而做的优化
s := S21{N1: 100, N2: 120, N3: 0, N4: 1}
fmt.Printf("s地址:%p, s.N1地址:%p, s.N2地址:%p, s.N3地址:%p, s.N4地址:%p\n", &s, &s.N1, &s.N2, &s.N3, &s.N4)
// s23中所有属性的地址都是连续的
e := S23{m:S22{1,2}, n:S22{3,4}}
fmt.Printf("e地址:%p, e.m.x地址:%p, e.m.y地址:%p, e.n.x地址:%p, e.n.y地址:%p\n",
&e, &e.m.x, &e.m.y, &e.n.x, &e.n.y)
// s24中m和n的地址连续,但是m和n指针对应的s22的地址就不一定连续了
f := S24{m:&S22{x:1,y:2},n:&S22{x:3,y:4}}
fmt.Printf("f地址:%p, f.m地址:%p, f.n地址:%p\n",&f, &f.m, &f.n)
fmt.Printf("f地址:%p, f.m.x地址:%p, f.m.y地址:%p, f.n.x地址:%p, f.n.y地址:%p\n",
&f, &f.m.x, &f.m.y, &f.n.x, &f.n.y)
}
func main() {
f21()
}
[root@heyingsheng 13-notice]# go run 02.go
s地址:0xc00000c380, s.N1地址:0xc00000c380, s.N2地址:0xc00000c381, s.N3地址:0xc00000c388, s.N4地址:0xc00000c390
e地址:0xc0000100a8, e.m.x地址:0xc0000100a8, e.m.y地址:0xc0000100a9, e.n.x地址:0xc0000100aa, e.n.y地址:0xc0000100ab
f地址:0xc0000461f0, f.m地址:0xc0000461f0, f.n地址:0xc0000461f8
f地址:0xc0000461f0, f.m.x地址:0xc0000100ac, f.m.y地址:0xc0000100ad, f.n.x地址:0xc0000100ae, f.n.y地址:0xc0000100af
2.5.3. 结构体之间类型转换
两个定义完全相同的结构体仍然是不同的数据类型,不能直接做类型转换。当且仅当在两个结构体字段完全相同(字段名称、类型、个数相同)的情况下实现类型强制转换!
package main
import "fmt"
type S31 struct {
Name string
Age int
}
type S32 struct {
Name string
Age int
}
func main() {
s1 := S31{Name:"Tom", Age:18}
s2 := S32{}
//s2 = s1 // 直接直接转换报错: cannot use s1 (type S31) as type S32 in assignment
s2 = S32(s1)
fmt.Printf("s1类型:%T, s2类型:%T\n", s1, s2)
fmt.Printf("s1的值:%v, s2的值:%v\n", s1, s2)
}
[root@heyingsheng 13-notice]# go run 03.go
s1类型:main.S31, s2类型:main.S32
s1的值:{Tom 18}, s2的值:{Tom 18}
3. 方法
方法是作用于特定数据类型变量的一种函数,本质是函数,只是相对于普通函数多了接收者信息。方法相当于Python中实例的方法,结构体的方法在编程中使用的非常多。格式如下:
func (接收者 接收者类型)函数名(参数)(返回值){
函数代码块
}
# 接收者类型:当前方法作用的数据类型,比如 service 结构体。也可以是数据类型的指针
# 接收者: 接收者相当于Py中方法的self参数,表示调用这个方法的实例。在go中使用类型名称的小写,如service类型使用s
# 函数名、参数、返回值与普通函数定义的规则一致
在方法中,采用 接收者.字段 可以实现调用实例的属性
# 指针类型接收者和值类型接收者区别
1. 指针类型接收者方法会用在较为复杂的数据类中,降低内存拷贝带来的资源消耗
2. 当需要修改实例的属性时,需要通过指针类型的方法
3. 一般非特殊场景中,建议使用指针类型的方法
# 语法糖
如果指针类型的变量调用值类型接收者方法时,方法会自动对指针求值!
3.1. 值类型接收者
3.1.1. 代码展示
package main
import "fmt"
type service struct {
name string
port []uint16
man string
}
func newService(name string,port []uint16,man string) *service {
return &service{
name: name,
port: port,
man: man,
}
}
func (s service)startService() {
s.port = []uint16{3366}
fmt.Printf("startService: %T %p\n", s, &s)
fmt.Printf("startService: service %s is started!Listen port %v\n", s.name, s.port)
}
func main() {
mysql := newService("MySQL", []uint16{3306},"")
fmt.Printf("%T %p %v\n", mysql, mysql,mysql)
mysql.startService()
fmt.Printf("%T %p %v\n", mysql, mysql,mysql)
}
[root@heyingsheng day04]# go run 06-method/main.go // 端口没有发生变化
*main.service 0xc00004e040 &{MySQL [3306] }
startService: main.service 0xc00004e0c0
startService: service MySQL is started!Listen port [3366]
*main.service 0xc00004e040 &{MySQL [3306] }
3.1.2. 执行步骤分析
3.2. 指针类型接收者
3.2.1. 代码展示
package main
import "fmt"
type service struct {
name string
port []uint16
man string
}
func newService(name string,port []uint16,man string) *service {
return &service{
name: name,
port: port,
man: man,
}
}
func (s *service)stopService() {
s.port = []uint16{3377}
fmt.Printf("stopService: %T %p\n", s, s)
fmt.Printf("stopService: service %s is stop!Release port %v\n", s.name, s.port)
}
func main() {
mysql := newService("MySQL", []uint16{3306},"")
fmt.Printf("%T %p %v\n", mysql, mysql,mysql)
mysql.stopService()
fmt.Printf("%T %p %v\n", mysql, mysql,mysql)
}
[root@heyingsheng day04]# go run 06-method/main.go
*main.service 0xc00004e040 &{MySQL [3306] }
stopService: *main.service 0xc00004e040
stopService: service MySQL is stop!Release port [3377]
*main.service 0xc00004e040 &{MySQL [3377] }
3.2.2. 执行步骤分析
3.3. 特殊方法
3.3.1. String()
当一个数据类型实现了 String()
方法后,在使用 fmt.Println()
时,会自动调用该方法,类似于 Python 中的 __str__()
。
package main
import "fmt"
type Student struct {
Name string
Age int
}
func (s *Student)String() string {
ret := fmt.Sprintf("value:(Name:[%v], Age:[%v]); pointer:[%p]", s.Name, s.Age, s)
return ret
}
func main() {
s1 := Student{Name:"Tom", Age:20}
fmt.Println(&s1)
}
[root@heyingsheng 13-notice]# go run 04.go
value:(Name:[Tom], Age:[20]); pointer:[0xc0000044a0]
3.4. 给内置类型添加方法
自定义内置类型的用处之一在于为内置类型添加方法,如基于 string 类型自定义一个myStr类型,目的在于添加一些自定义方法,因为在其它包中是无法直接给内置类型添加方法的。
package main
import "fmt"
type myStr string
func (m myStr)repeat(n int) myStr {
var tmp myStr
for i:=1;i<=n;i++{
tmp += m
}
return tmp
}
func main() {
s0 := myStr("s")
s0 = s0.repeat(30)
fmt.Printf("tye:%T value:%v\n",s0,s0)
}
[root@heyingsheng day04]# go run 07-method/main.go
type:main.myStr value:ssssssssssssssssssssssssssssss
3.5. 方法和函数调用时的区别
3.5.1. 调用方式的区别
函数调用方式: 函数名(实参)
方法调用方式: 变量名.方法名(实参)
3.5.2. 方法中的语法糖
// 定义方法和结构体如下:
type Student struct { }
func (s Student)f1() { }
func (s *Student)f2() { }
s1 := Student{}
// 结构体实例 s1 可以直接调用 f1 和 f2,结构体指针 &s1 可以调用 f1 和 f2
s1.f1()
s1.f2()
(&s1).f1()
(&s1).f2()