标准库文档:Go语言标准库文档中文版 | Go语言中文网 | Golang中文社区 | Golang中国
B站课程:8小时转职Golang工程师(如果你想低成本学习Go语言)
课程作者语雀(首页有更多内容):8小时转职Golang工程师 · 语雀
代码仓库:GitHub - aceld/golang: 《Golang修养之路》本书针对Golang专题性热门技术深入理解,修养在Golang领域深入话题,脱胎换骨。
参考笔记:https://github.com/spongehah/golang-note/blob/main/Golang入门/Go入门.md
1. 环境
下载编译器
Go官网下载地址:https://golang.org/dl/
Go官方镜像站(推荐):https://golang.google.cn/
shell
# 打印Go语言的环境信息。包括Go的版本、GOROOT(Go的安装位置)、GOPATH(工作目录)等等
go env
修改配置
Go1.14版本之后,都推荐使用go mod
模式来管理依赖了,也不再强制我们把代码必须写在GOPATH
下面的src
目录了,可以在电脑的任意位置编写go代码。
默认GoPROXY配置是:GOPROXY=https://proxy.golang.org,direct
,国内访问不到 https://proxy.golang.org
所以需要换一个PROXY
,
这里推荐使用https://goproxy.io
或 https://goproxy.cn
。
可以执行下面的命令修改GOPROXY
:
shell
go env -w GOPROXY=https://goproxy.cn,direct
2. Golang语言特性
优势
++极简单的部署方式++
-
可直接编译成机器码
-
不依赖其他库
-
直接运行即可部署
++静态类型语言++
- 编译的时候检查出来隐藏的大多数问题
++语言层面的并发++
-
天生的基因支持
-
充分的利用多核
++强大的标准库++
-
runtime系统调度机制
-
高效的GC垃圾回收
-
丰富的标准库
进程、线程、Goroutine
底层库、加解密、email、应用构建、文本、debug、输入输出、数据结构算法、日期和时间、数学、文件系统、压缩、测试、数据持久存储与交换、同步机制、网络通信
简单易学
-
25个关键字
-
C语言简洁基因,内嵌C语法支持
-
面向对象特征(继承、多态、封装)
-
跨平台
劣势
1、包管理,大部分包都在github上
2、无泛化类型(Golang 1.18+已经支持泛型)
3、所有Excepiton 都用Error来处理(比较有争议)
4、对C 的降级处理,并非无缝,没有C 降级到asm那么完美(序列化问题)
Golang适合做什么
(1)、云计算基础设施领域
代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。
(2)、基础后端软件
代表项目:tidb、influxdb、cockroachdb等。
(3)、微服务
代表项目:go-kit、micro、monzo bank的typhon、bilibili等。
(4)、互联网基础设施
代表项目:以太坊、hyperledger等。
3. 语法
Hello world
go
// 定义包名。在源文件中非注释的第一行指明这个文件属于哪个包
// package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
package main
// 需要使用 fmt 包(的函数,或其他元素),该包实现了格式化 IO(输入/输出)的函数。
import "fmt"
// main 函数是每一个可执行程序所必须包含的,一般是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
func main() { // go语言的语法,定义函数的时候,'{' 必须和函数名在同一行,不能另起一行。
/* 简单的程序 万能的hello world */
fmt.Println("Hello Go")
}
终端运行
shell
# 直接编译并执行
go run 文件名.go
# 也可以 先编译再执行
go build test1_hello.go
./test1_hello
3.1 变量
一般使用 var 关键字
单变量声明
go
package main
import "fmt"
// 声明全局变量
var gA int = 100
var gB = "gBtest"
func main() {
// 方式一:指定变量类型,声明后若不赋值,默认值是0。
var a int
fmt.Println("a = ", a)
fmt.Printf("type of a = %T\n", a)
// 方式二:声明变量同时初始化值
var b int = 10
fmt.Println("b = ", b)
fmt.Printf("type of b = %T\n", b)
// 方式三:省略数据类型,自动匹配类型
var c = 20
fmt.Println("c = ", c)
fmt.Printf("type of c = %T\n", c)
var cc = "abcd"
fmt.Printf("cc = %s, type of c = %T\n", cc, cc)
// 方式四(最常用):省略var关键字,但只能用于在函数体内声明变量,不可用于声明全局变量
d := 3.14
fmt.Println("d = ", d)
fmt.Printf("d = %f\n", d)
// 使用全局变量
fmt.Println("gA = ", gA, ", gB = ", gB)
}
多变量声明
go
package main
import "fmt"
func main() {
// 多变量声明 - 单行写法
var xx, yy int = 100, 200
fmt.Println("xx = ", xx, ", yy = ", yy)
var kk, ll = 100, "lltest"
fmt.Println("kk = ", kk, ", ll = ", ll)
// 多变量声明 - 多行写法
// 但这种分解的写法一般用于声明全局变量
var (
vv int = 100
jj bool = true
)
fmt.Println("vv = ", vv, ", jj = ", jj)
}
注意
go
_, value := 7, 5 // 实际上7的赋值被废弃,变量 _ 不具备读特性
//fmt.Println(_) // 变量 _ 是读不出来的
fmt.Println(value) //5
3.2 常量
使用 const 关键字
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
go
// 常量的定义格式,可省略类型说明符 [type],因为编译器可根据变量的值来推断其类型
const identifier [type] = value
// 显式定义
const b string = "abc"
// 隐式定义
const b = "abc"
// 多重赋值
const a, b, c = 1, false, "str"
常量可以用作枚举
go
const (
Unknown = 0
Female = 1
Male = 2
)
常量可以用len(), cap(), unsafe.Sizeof()
常量计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过
go
package main
import "unsafe"
const (
a = "abc"
b = len(a)
// unsafe.Sizeof(a)的结果是16。字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。
c = unsafe.Sizeof(a)
)
func main(){
// 输出结果为:abc, 3, 16
println(a, b, c)
}
3.3 iota
iota [aɪˈəʊtə] 极少量;微量;希腊字母表的第9个字母
iota 只能够配合const()
一起使用, iota只有在const进行累加效果。
自增长
使用iota
标示符,可简化常量用于增长数字的定义。
go
const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)
表达式
go
type Allergen int
const (
IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001
IgChocolate // 1 << 1 which is 00000010
IgNuts // 1 << 2 which is 00000100
IgStrawberries // 1 << 3 which is 00001000
IgShellfish // 1 << 4 which is 00010000
)
type ByteSize float64
const (
_ = iota // ignore first value by assigning to blank identifier
KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
MB // 1 << (10*2)
GB // 1 << (10*3)
TB // 1 << (10*4)
PB // 1 << (10*5)
EB // 1 << (10*6)
ZB // 1 << (10*7)
YB // 1 << (10*8)
)
// 把两个常量定义在一行的时
const (
Apple, Banana = iota + 1, iota + 2 // Apple:1 Banana:2
Cherimoya, Durian // Cherimoya:2 Durian:3
Elderberry, Fig // Elderberry:3 Fig:4
)
demo
go
package main
import "fmt"
// const 来定义枚举类型
const (
// 可以在const() 添加一个关键字 iota, 每行的iota都会累加1, 第一行的iota的默认值是0
BEIJING = 10 * iota // iota = 0
SHANGHAI // iota = 1
SHENZHEN // iota = 2
)
const (
a, b = iota + 1, iota + 2 // iota = 0, a = iota + 1, b = iota + 2, a = 1, b = 2
c, d // iota = 1, c = iota + 1, d = iota + 2, c = 2, d = 3
e, f // iota = 2, e = iota + 1, f = iota + 2, e = 3, f = 4
g, h = iota * 2, iota * 3 // iota = 3, g = iota * 2, h = iota * 3, g = 6, h = 9
i, k // iota = 4, i = iota * 2, k = iota * 3 , i = 8, k = 12
)
func main() {
// 常量(只读属性)
const length int = 10
fmt.Println("length = ", length)
// 常量不允许修改
// length = 100
fmt.Println("BEIJIGN = ", BEIJING)
fmt.Println("SHANGHAI = ", SHANGHAI)
fmt.Println("SHENZHEN = ", SHENZHEN)
fmt.Println("a = ", a, "b = ", b)
fmt.Println("c = ", c, "d = ", d)
fmt.Println("e = ", e, "f = ", f)
fmt.Println("g = ", g, "h = ", h)
fmt.Println("i = ", i, "k = ", k)
// iota 只能够配合const() 一起使用, iota只有在const进行累加效果。
// var a int = iota
}
3.4 函数
使用 func
关键字定义函数,Go的函数可以有多个返回值
使用 func() {}()
来声明并调用一个匿名函数
go
package main
import "fmt"
// 例1 简单的
func foo1(a string, b int) int {
fmt.Println("a = ", a)
fmt.Println("b = ", b)
c := 100
return c
}
// 例2 返回多个返回值,匿名的
func foo2(a string, b int) (int, int) {
fmt.Println("a = ", a)
fmt.Println("b = ", b)
return 666, 777
}
// 例3 返回多个返回值,有形参名称的
func foo3(a string, b int) (r1 int, r2 int) {
fmt.Println("---- foo3 ----")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
// r1 r2 初始化默认的值是0
// r1 r2 作用域空间 是foo3 整个函数体的{}空间
fmt.Println("r1 = ", r1)
fmt.Println("r2 = ", r2)
// 给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
return
}
// 例4 在例3的基础上 返回类型都是int可以合并
func foo4(a string, b int) (r1, r2 int) {
fmt.Println("---- foo4 ----")
fmt.Println("a = ", a)
fmt.Println("b = ", b)
// 给有名称的返回值变量赋值时也可以这样直接返回
return 1000, 2000
}
func main() {
c := foo1("abc", 555)
fmt.Println("c = ", c)
ret1, ret2 := foo2("haha", 999)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)
ret1, ret2 = foo3("foo3", 333)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)
ret1, ret2 = foo4("foo4", 444)
fmt.Println("ret1 = ", ret1, " ret2 = ", ret2)
}
值传递
Go 语言默认使用的是值传递,即在调用过程中不会影响到实际参数。
go
package main
import "fmt"
func changeValue(p int) {
p = 10
}
func main() {
var a int = 1
changeValue(a)
fmt.Println("a =", a) //a= 1
}
引用传递
go
package main
import "fmt"
func changeValue(p *int) {
*p = 10
}
func main() {
var a int = 1
changeValue(&a)
fmt.Println("a =", a)
}
3.5 import导包与init方法
│ main.go
│
├─lib1
│ Lib1.go
│
└─lib2
Lib2.go
新版本需要关闭go mod:go env -w GO111MODULE=off
Lib1.go
go
package lib1
import "fmt"
// 模块中要导出的函数,必须首字母大写
// 当前lib1包提供的API
func Lib1Test() {
fmt.Println("lib1Test()...")
}
func init() {
fmt.Println("lib1.init() ...")
}
Lib2.go
go
package lib2
import "fmt"
// 模块中要导出的函数,必须首字母大写
// 当前lib2包提供的API
func Lib2Test() {
fmt.Println("lib2Test()...")
}
func init() {
fmt.Println("lib2.init() ...")
}
Main.go
go
package main
import (
"GolangStudy/5-init/lib1"
"GolangStudy/5-init/lib2"
)
func main() {
lib1.Lib1Test()
lib2.Lib2Test()
}
import导包的三种方式
import _ "fmt"
:给fmt包起一个别名,匿名,无法使用该包的方法,但是会执行该的包内部的init()方法
import aa "fmt"
:给fmt包起一个别名,aa,aa.println()
直接调用
import . "fmt"
:将fmt包中的全部方法,导入到当前本包的作用域中,fmt包中的全部的方法可以直接使用API来调用,不需要fmt.API
来调用。但遇到两个包内方法同名的情况会报错。
3.6 defer延迟调用
defer
用于注册延迟调用,这些调用在 return 后被执行。
可以用做【资源清理(关闭文件、释放锁、断开连接等)、捕捉处理异常、输出日志】等。
多个defer的执行顺序:LIFO(先进后出,栈的特性)。
go
package main
import "fmt"
// 多个defer的执行顺序
func main() {
defer fmt.Println("main::end1")
defer fmt.Println("main::end2")
fmt.Println("hello! go1")
fmt.Println("hello! go2")
}
// 输出顺序
// hello! go1
// hello! go2
// main::end2
// main::end1
recover错误拦截
运行时panic异常一旦被引发就会导致程序崩溃。
Go语言提供了专用于"拦截"运行时panic的内建函数"recover"。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
注意:recover只有在defer调用的函数中有效。
go
package main
import "fmt"
func Demo(i int) {
// 定义10个元素的数组
var arr [10]int
// 错误拦截要在产生错误前设置
defer func() {
// 设置recover拦截错误信息
err := recover()
// 产生panic异常 打印错误信息
if err != nil {
fmt.Println(err)
}
}()
// 根据函数参数为数组元素赋值
// 如果i的值超过数组下标 会报错误:数组下标越界
arr[i] = 10
}
func main() {
Demo(10)
//产生错误后 程序继续
fmt.Println("程序继续执行...")
}
输出结果
runtime error: index out of range [10] with length 10
程序继续执行...
3.7 slice
固定长度的数组
go
package main
import "fmt"
func main() {
// 固定长度的数组
var myArray1 [10]int
// for i := 0; i< 10; i++ {
for i := 0; i < len(myArray1); i++ {
// 会打印出10个为0的元素
fmt.Println(myArray1[i])
}
// 如果是数组或切片这种动态数组,range会返回两个值,第一个值是当前元素所在的下标,第二个值是当前元素的值本身
myArray2 := [6]int{1, 2, 3, 4}
for index, value := range myArray2 {
// index = 0 value = 1
// index = 1 value = 2
// index = 2 value = 3
// index = 3 value = 4
// index = 4 value = 0
// index = 5 value = 0
fmt.Println("index = ", index, "value = ", value)
}
// myArray1 types = [10]int
// myArray2 types = [6]int
fmt.Printf("myArray1 types = %T\n", myArray1)
fmt.Printf("myArray2 types = %T\n", myArray2)
}
range会根据遍历的不同的集合来返回不同的值
固定长度的数组在用函数形参接收时,长度也得一样func printArray(myArray [6]int)
,并且是值拷贝
动态数组(切片slice)
切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组")
,
与数组相比,切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
go
package main
import "fmt"
func printArray(myArray []int) {
// 引用传递
// _ 表示匿名的变量
for _, value := range myArray {
fmt.Println("value = ", value)
}
// 引用传递,修改起作用
myArray[0] = 100
}
func main() {
// 动态数组,切片 slice
myArray := []int{1, 2, 3, 4}
// myArray type is []int
fmt.Printf("myArray type is %T\n", myArray)
printArray(myArray)
fmt.Println(" ==== ")
for _, value := range myArray {
fmt.Println("value = ", value)
}
}
定义切片
var identifier []type
- 直接声明并初始化赋值:
slice1 := []int{1, 2, 3}
- 只声明:
var slice1 []int
,一个切片在未初始化时默认为nil
,长度为 0 - 声明并分配空间:
var slice1 []int = make([]int, 3)
- 声明并分配空间,通过
:=
推导:slice1 := make([]int, 3)
go
package main
import "fmt"
func main() {
// 1 - 声明slice1是一个切片,并初始化,默认值是1,2,3。 长度len是3
// slice1 := []int{1, 2, 3}
// 2 - 声明slice1是一个切片,但没给分配空间。一个切片在未初始化时默认为 nil,长度为 0
var slice1 []int
// slice1 = make([]int, 3) // 开辟3个空间 ,默认值是0
// 3 - (将方式2合并步骤)声明slice1是一个切片,同时给slice分配3个空间,初始化值是0
// var slice1 []int = make([]int, 3)
// 4 - 声明slice1是一个切片,同时给slice分配空间,3个空间,初始化值是0, 通过:=推导出slice是一个切片
// slice1 := make([]int, 3)
fmt.Printf("len = %d, slice = %v\n", len(slice1), slice1)
// 判断一个silce是否为0
if slice1 == nil {
fmt.Println("slice1 是一个空切片")
} else {
fmt.Println("slice1 是有空间的")
}
}
切片追加append()与扩容(涉及len和cap)
make(type, len, capacity)
这个函数,当只传两个参数时,capacity = len。
numbers := make([]int, 3, 5)
声明一个长度为3,容量为5的切片。
通过numbers = append(numbers, 1)
向切片中追加元素,直到容量cap。
容量cap已满后,若继续向切片中追加元素,cap将会扩容为原来的2倍
append()
可以同时追加多个元素append(numbers, 2,3,4)
go
package main
import "fmt"
func main() {
var numbers = make([]int, 3, 5)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 向numbers切片追加一个元素1, numbers len = 4, [0,0,0,1], cap = 5
numbers = append(numbers, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 向numbers切片追加一个元素2, numbers len = 5, [0,0,0,1,2], cap = 5
numbers = append(numbers, 2)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
// 向一个容量cap已经满的slice 追加元素
numbers = append(numbers, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers), cap(numbers), numbers)
fmt.Println("--------")
var numbers2 = make([]int, 3)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2), numbers2)
numbers2 = append(numbers2, 1)
fmt.Printf("len = %d, cap = %d, slice = %v\n", len(numbers2), cap(numbers2), numbers2)
}
切片的截取
go
package main
import "fmt"
func main() {
s := []int{1, 2, 3} //len = 3, cap = 3, [1,2,3]
// 截取[0, 2)
s1 := s[0:2]
fmt.Println(s1)
s1[0] = 100
// s和s1的值都会被改掉
fmt.Println(s)
fmt.Println(s1)
// copy 可以将底层数组的slice一起进行拷贝
s2 := make([]int, 3) // s2 = [0,0,0]
// 将s中的值 依次拷贝到s2中
copy(s2, s)
fmt.Println(s2)
}
其他例子
go
// 使用make()函数来创建切片
var slice1 []type = make([]type, len)
// 也可以简写为
slice1 := make([]type, len)
// 也可以指定容量,其中capacity为可选参数。len 是数组的长度并且也是切片的初始长度
make([]T, length, capacity)
// 切片初始化
// 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3,其cap=len=3
s := []int {1,2,3}
// 初始化切片s,是数组arr的引用
s := arr[:]
// 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:endIndex]
// 缺省endIndex时将表示一直到arr的最后一个元素
s := arr[startIndex:]
// 缺省startIndex时将表示从arr的第一个元素开始
s := arr[:endIndex]
// 通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
// 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
s := make([]int,len,cap)
3.8 map
map和slice类似,只不过是数据结构不同。
map内部是哈希,不是顺序排序的。
定义map
go
package main
import (
"fmt"
)
func main() {
// 声明方式1
// 声明myMap1是一种map类型 key是string,value是string
var myMap1 map[string]string
if myMap1 == nil {
fmt.Println("myMap1 是一个空map")
}
// 在使用map前,需先用make给map分配数据空间
myMap1 = make(map[string]string, 10)
myMap1["one"] = "java"
myMap1["two"] = "c++"
myMap1["three"] = "python"
fmt.Println(myMap1)
// 声明方式2
myMap2 := make(map[int]string)
myMap2[1] = "java"
myMap2[2] = "c++"
myMap2[3] = "python"
fmt.Println(myMap2)
// 声明方式3
myMap3 := map[string]string{
"one": "php",
"two": "c++",
"three": "python",
}
fmt.Println(myMap3)
}
打印结果
go
myMap1 是一个空map
map[one:java three:python two:c++]
map[1:java 2:c++ 3:python]
map[one:php three:python two:c++]
map基本操作
go
package main
import (
"fmt"
)
// 遍历map:引用传递
func printMap(cityMap map[string]string) {
for k, v := range cityMap {
fmt.Println("k = ", k, ", v = ", v)
}
}
func main() {
// 声明
cityMap := make(map[string]string)
// 添加
cityMap["China"] = "Beijing"
cityMap["Japan"] = "Tokyo"
cityMap["USA"] = "New York"
// 遍历
printMap(cityMap)
// 删除
delete(cityMap, "China")
// 修改
cityMap["USA"] = "DC"
fmt.Println("--------")
printMap(cityMap)
}
3.9 OOP
结构体
type
关键字给类型声明别名,声明一种新的数据类型 myint:type myint int
type
结合struct
关键字可以定义一个结构体,结构体对函数参数的传递默认是值传递,只有显示使用指针时,才是引用传递
go
// 定义一个结构体
type Book struct {
title string
auth string
}
方法
go
package main
import "fmt"
// 声明一种行的数据类型 myint, 是int的一个别名
type myint int
// 定义一个结构体
type Book struct {
title string
auth string
}
func changeBook(book Book) {
// 值传递:传递一个book的副本
book.auth = "666"
}
func changeBook2(book *Book) {
// 引用传递:指针传递
book.auth = "777"
}
func main() {
var book1 Book
book1.title = "Golang"
book1.auth = "zhang3"
fmt.Printf("%v\n", book1)
changeBook(book1)
fmt.Printf("%v\n", book1)
changeBook2(&book1)
fmt.Printf("%v\n", book1)
}
方法值和方法表达式
方法值
我们经常选择一个方法,并且在同一个表达式里执行,比如常见的p.Distance()形式,实际上将其分成两步来执行也是可能的。p.Distance叫作"选择器",选择器会返回一个方法"值"一个将方法(Point.Distance)绑定到特定接收器变量的函数。这个函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器,只要传入函数的参数即可:
类的封装
上面的结构体其实就是一个类class,只是声明了它的属性,但是没有封装它的方法
func (thisClass Class) funcName
:thisClass是值传递,是对象的值克隆/拷贝func (thisClass *Class) funcName
:thisClass是引用传递,传递的是对象的地址
类名、属性名、方法名 首字母大写表示对外(其他包)开放访问,否则只能在本包内访问。
当调用
hero.SetName1()
时相当于SetName1(hero)
,实参和形参都是类型Hero
,可以接受。此时在SetName1()
中的thisHero
只是参数hero
的值拷贝,所以SetName1()
的修改不影响main中的hero变量。当调用
hero.SetName2()
=>SetName2(hero)
,这是将Hero
类型传给了*Hero
类型,go可能会取Hero
的地址传进去:SetName2(&hero)
。所以SetName2()
的修改可以影响main中的hero变量。
Hero
类型的变量这两个方法都是拥有的。
go
package main
import "fmt"
// 类名首字母大写,表示其他包也能够访问
type Hero struct {
// 类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问
Name string
Ad int
level int
}
func (thisHero Hero) Show() {
// 值传递:thisHero 只是调用该方法的对象的一个副本(拷贝)
fmt.Println("Name = ", thisHero.Name)
}
// (thisHero Hero)表示绑定到当前对象的Hero结构体
// 方法名大写,代表其它包也能访问
func (thisHero Hero) SetName1(newName string) {
// 值传递:thisHero 只是调用该方法的对象的一个副本(拷贝)
thisHero.Name = newName
}
func (thisHero *Hero) SetName2(newName string) {
// 引用传递:thisHero指向hero对象的地址
thisHero.Name = newName
}
func main() {
// 创建一个对象
hero := Hero{Name: "zhangsan", Ad: 100}
hero.Show()
// 调用方法
hero.SetName2("lisi")
hero.Show()
}
类的继承
继承可以重写父类的方法,也可以新增额外的属性和方法
go
package main
import "fmt"
type Human struct {
name string
sex string
}
func (this *Human) Walk() {
fmt.Println("Human.Walk()...")
}
func (this *Human) Eat() {
fmt.Println("Human.Eat()...")
}
//=================
type SuperMan struct {
Human // SuperMan类继承了Human类的方法
level int
}
// 重定义父类的方法Eat()
func (this *SuperMan) Eat() {
fmt.Println("SuperMan.Eat()...")
}
//=================
func main() {
h := Human{"zhang3", "female"}
h.Eat()
h.Walk()
// 定义一个子类对象
// s := SuperMan{Human{"li4", "female"}, 88}
var s SuperMan
s.name = "li4"
s.sex = "male"
s.level = 88
s.Eat() // 子类的方法
}
类的多态
Go语言中,多态性只能通过 接口interface 来实现,不能通过 类class 来实现
Go中interface本质是一个指针,会自动指向对应的实现类
一个类实现了某个接口的条件:该类重写 了接口中声明的所有方法 ,则自动实现了该接口
因为接口本质是一个指针,所以在 声明接口变量后,对接口变量进行实现类的赋值,需要赋值的是对象地址
go
package main
import "fmt"
// interface本质是一个指针
type AnimalIF interface {
Sleep()
GetColor() string // 获取动物的颜色
}
// 具体的类
type Cat struct {
color string // 猫的颜色
}
func (this *Cat) Sleep() {
fmt.Println("Cat is Sleep")
}
func (this *Cat) GetColor() string {
return this.color
}
// 具体的类
type Dog struct {
color string
}
func (this *Dog) Sleep() {
fmt.Println("Dog is Sleep")
}
func (this *Dog) GetColor() string {
return this.color
}
func showAnimal(animal AnimalIF) {
animal.Sleep() // 多态
fmt.Println("color = ", animal.GetColor())
}
func main() {
var animal AnimalIF // 接口的数据类型,父类指针
animal = &Cat{"Green"}
animal.Sleep() // 调用的就是Cat的Sleep()方法,多态的现象
animal = &Dog{"Yellow"}
animal.Sleep() // 调用Dog的Sleep方法,多态的现象
// ---------
cat := Cat{"Green"}
dog := Dog{"Yellow"}
showAnimal(&cat)
showAnimal(&dog)
}
3.10 interface{}和断言
在Go中,所有数据类型都会实现这个接口:万能数据类型interface{}
,它可以通过多态传递任何一种数据类型
在Go1.18中,新引入关键字 any,any == interface{}
interface{}、any 类似于Java中的Object
断言 类似于Java中的强制类型转换
断言:
- 使用 any类型的数据 .(string),可以获得两个变量 value, ok ,分别代表 值 和 是否断言成功
- 可以在switch中使用 .(type) 来判断其类型
注:断言后,变量的类型并未改变,反射的pair部分会详细讲解
go
package main
import "fmt"
// interface{}是万能数据类型
// func myFunc(arg interface{}) {
func myFunc(arg any) {
fmt.Println("myFunc is called...")
fmt.Println(arg)
// interface{} 如何区分此时引用的底层数据类型是什么?
// interface{} 提供了 "类型断言" 的机制
// 但若断言失败一般会导致panic的发生,所以要在断言前进行一定的判断 value, ok := arg.(string)。ok的值就代表断言成功与否
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string type")
} else {
fmt.Println("arg is string type, value = ", value)
fmt.Printf("value type is %T\n", value)
}
}
type Book struct {
auth string
}
func main() {
book := Book{"Golang"}
myFunc(book)
myFunc(100)
myFunc("abc")
myFunc(3.14)
}
断言配合switch使用
go
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
3.11 反射 //todo 待补充
基础概念pair
Golang关于类型设计的一些原则
-
变量:一个变量是一个
pair(type, value)
对- type 实际变量类型
- static type:编码时看见的类型(如int、string)
- concrete type:运行时系统看见的类型(interface类型)
- value 实际变量值
- type 实际变量类型
-
类型断言能否成功,取决于变量的
concrete type
,而不是static type
。因此,一个reader
变量如果它的concrete type
也实现了write
方法的话,它也可以被类型断言为writer
。
++static type
在创建变量的时候就已经确定,而concrete type
才与反射有关++
Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,
反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。
一个interface{}
类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。
interface{}类型的变量,在赋值过程中,pair中的type始终保持不变
即断言后,变量的类型并未改变
以下三个demo说明:pair中的type在赋值过程中保持不变
demo1
go
package main
import "fmt"
func main() {
var a string
// pair<statictype:string, value:"aceld">
a = "aceld"
var allType interface{}
// pair<type:string, value:"aceld">
allType = a
str, _ := allType.(string)
fmt.Println(str)
}
demo2
go
package main
import (
"fmt"
"io"
"os"
)
func main() {
// 创建类型为*os.File的变量tty。【/dev/tty】表示linux终端,【os.O_RDWR】可读可写
// tty: pair<type:*os.File, value:"/dev/tty"文件描述符>。tty不管赋值给谁,其pair是不变的。
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
fmt.Println("open file error", err)
return
}
// r: pair<type: , value: >
var r io.Reader
// 将tty赋给一个接口变量r,它的pair在接口变量的连续赋值过程中是不变的
// r: pair<type:*os.File, value:"/dev/tty"文件描述符>
r = tty
// w: pair<type: , value: >
var w io.Writer
// 接口变量w的pair与r的pair相同,即使w是空接口类型,pair也是不变的。
// w: pair<type:*os.File, value:"/dev/tty"文件描述符>
w = r.(io.Writer)
w.Write([]byte("HELLO!\n"))
}
demo3
go
package main
import "fmt"
type Reader interface {
ReadBook()
}
type Writer interface {
WriteBook()
}
// 具体类型
type Book struct {
}
func (this *Book) ReadBook() {
fmt.Println("Read a book.")
}
func (this *Book) WriteBook() {
fmt.Println("Write a book.")
}
func main() {
// b: pair<type:Book, value:Book{}地址>
b := &Book{}
// r: pair<type: , value: >
var r Reader
// r: pair<type:Book, value:Book{}地址>
r = b
r.ReadBook()
var w Writer
// w: pair<type:Book, value:Book{}地址>
// 断言有两步:得到动态类型 type,判断 type 是否实现了目标接口。这里断言成功是因为 type 是 Book,而 Book 实现了 Writer 接口
w = r.(Writer)
w.WriteBook()
}
interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
反射reflect
想直接获取到变量内部的信息,Golang的reflect反射包中提供了reflect.ValueOf()
和reflect.TypeOf()
两种类型(或者说两个方法)可以轻松访问接口变量内容
go
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}
// ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}
// TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil
reflect.TypeOf()
是获取pair中的type,reflect.ValueOf()
获取pair中的value,示例:
go
package main
import (
"fmt"
"reflect"
)
func main() {
var num float64 = 1.2345
// type: float64
fmt.Println("type: ", reflect.TypeOf(num))
// value: 1.2345
fmt.Println("value: ", reflect.ValueOf(num))
}
也就是说明反射可以将"接口类型变量"转换为"反射类型对象",反射类型指的是reflect.Type和reflect.Value这两种
go
package main
import (
"fmt"
"reflect"
)
type User struct {
Id int
Name string
Age int
}
func (this User) Call() {
fmt.Println("user is called ..")
fmt.Printf("%v\n", this)
}
func DoFiledAndMethod(input interface{}) {
// 获取input的type
inputType := reflect.TypeOf(input)
fmt.Println("inputType is :", inputType.Name())
// 获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue is:", inputValue)
// 通过 type 获取里面的字段
// 1. 获取interface的reflect.Type,通过Type得到NumField, 即字段的个数 ,进行遍历
// 2. 通过下标 i 得到每个field,数据类型Type,可以获得该Type的 类型 和 字段名
// 3. 通过Filed有一个Interface()方法得到 对应的value
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()
fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}
// 通过type 获取里面的方法,调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
// 调用方法
m.Func.Call([]reflect.Value{inputValue})
}
}
func main() {
user := User{1, "Aceld", 18}
DoFiledAndMethod(user)
}
3.12 结构体标签
go
package main
import (
"fmt"
"reflect"
)
type resume struct {
Name string `info:"name" doc:"我的名字"`
Sex string `info:"sex"`
}
func findTag(input any) {
// 当前结构体的全部元素
// elem := reflect.TypeOf(input).Elem()
elem := reflect.TypeOf(input)
for i := 0; i < elem.NumField(); i++ {
tagInfo := elem.Field(i).Tag.Get("info")
tagDoc := elem.Field(i).Tag.Get("doc")
fmt.Println("tagInfo: ", tagInfo, ", tagDoc: ", tagDoc)
}
}
func main() {
var re resume
// 若使用reflect.TypeOf(input).Elem(),此处要传地址
// findTag(&re)
findTag(re)
}
结构体标签在json中的应用
go
package main
import (
"encoding/json"
"fmt"
)
type Movie struct {
Title string `json:"title"`
Year int `json:"year"`
Price int `json:"rmb"`
Actors []string `json:"actors"`
}
func main() {
movie := Movie{"喜剧之王", 2000, 10, []string{"xingye", "zhangbozhi"}}
// 编码的过程 结构体---> json
jsonStr, err := json.Marshal(movie)
if err != nil {
fmt.Println("json marshal error", err)
return
}
fmt.Printf("jsonStr = %s\n", jsonStr)
// 解码的过程 jsonstr ---> 结构体
// jsonStr = {"title":"喜剧之王","year":2000,"rmb":10,"actors":["xingye","zhangbozhi"]}
myMovie := Movie{}
err = json.Unmarshal(jsonStr, &myMovie)
if err != nil {
fmt.Println("json unmarshal error ", err)
return
}
fmt.Printf("%v\n", myMovie)
}