标准库文档: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%E5%85%A5%E9%97%A8/Go%E5%85%A5%E9%97%A8.md
1. 环境
下载编译器
Go官网下载地址:https://golang.org/dl/
Go官方镜像站(推荐):https://golang.google.cn/
# 打印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
:
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
// 定义包名。在源文件中非注释的第一行指明这个文件属于哪个包
// package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包
package main// 需要使用 fmt 包(的函数,或其他元素),该包实现了格式化 IO(输入/输出)的函数。
import "fmt"// main 函数是每一个可执行程序所必须包含的,一般是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
func main() { // go语言的语法,定义函数的时候,‘{’ 必须和函数名在同一行,不能另起一行。/* 简单的程序 万能的hello world */fmt.Println("Hello Go")
}
终端运行
# 直接编译并执行
go run 文件名.go
# 也可以 先编译再执行
go build test1_hello.go
./test1_hello
3.1 变量
一般使用 var 关键字
单变量声明
package mainimport "fmt"// 声明全局变量
var gA int = 100
var gB = "gBtest"func main() {// 方式一:指定变量类型,声明后若不赋值,默认值是0。var a intfmt.Println("a = ", a)fmt.Printf("type of a = %T\n", a)// 方式二:声明变量同时初始化值var b int = 10fmt.Println("b = ", b)fmt.Printf("type of b = %T\n", b)// 方式三:省略数据类型,自动匹配类型var c = 20fmt.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.14fmt.Println("d = ", d)fmt.Printf("d = %f\n", d)// 使用全局变量fmt.Println("gA = ", gA, ", gB = ", gB)
}
多变量声明
package mainimport "fmt"func main() {// 多变量声明 - 单行写法var xx, yy int = 100, 200fmt.Println("xx = ", xx, ", yy = ", yy)var kk, ll = 100, "lltest"fmt.Println("kk = ", kk, ", ll = ", ll)// 多变量声明 - 多行写法// 但这种分解的写法一般用于声明全局变量var (vv int = 100jj bool = true)fmt.Println("vv = ", vv, ", jj = ", jj)}
注意
_, value := 7, 5 // 实际上7的赋值被废弃,变量 _ 不具备读特性//fmt.Println(_) // 变量 _ 是读不出来的fmt.Println(value) //5
3.2 常量
使用 const 关键字
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
// 常量的定义格式,可省略类型说明符 [type],因为编译器可根据变量的值来推断其类型
const identifier [type] = value
// 显式定义
const b string = "abc"
// 隐式定义
const b = "abc"
// 多重赋值
const a, b, c = 1, false, "str"
常量可以用作枚举
const (Unknown = 0Female = 1Male = 2
)
常量可以用len(), cap(), unsafe.Sizeof()
常量计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过
package mainimport "unsafe"const (a = "abc"b = len(a)// unsafe.Sizeof(a)的结果是16。字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。c = unsafe.Sizeof(a)
)func main(){// 输出结果为:abc, 3, 16println(a, b, c)
}
3.3 iota
iota [aɪˈəʊtə] 极少量;微量;希腊字母表的第9个字母
iota 只能够配合const()
一起使用, iota只有在const进行累加效果。
自增长
使用iota
标示符,可简化常量用于增长数字的定义。
const (CategoryBooks = iota // 0CategoryHealth // 1CategoryClothing // 2
)
表达式
type Allergen int
const (IgEggs Allergen = 1 << iota // 1 << 0 which is 00000001IgChocolate // 1 << 1 which is 00000010IgNuts // 1 << 2 which is 00000100IgStrawberries // 1 << 3 which is 00001000IgShellfish // 1 << 4 which is 00010000
)type ByteSize float64
const (_ = iota // ignore first value by assigning to blank identifierKB 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:2Cherimoya, Durian // Cherimoya:2 Durian:3Elderberry, Fig // Elderberry:3 Fig:4
)
demo
package mainimport "fmt"// const 来定义枚举类型
const (// 可以在const() 添加一个关键字 iota, 每行的iota都会累加1, 第一行的iota的默认值是0BEIJING = 10 * iota // iota = 0SHANGHAI // iota = 1SHENZHEN // iota = 2
)const (a, b = iota + 1, iota + 2 // iota = 0, a = iota + 1, b = iota + 2, a = 1, b = 2c, d // iota = 1, c = iota + 1, d = iota + 2, c = 2, d = 3e, f // iota = 2, e = iota + 1, f = iota + 2, e = 3, f = 4g, h = iota * 2, iota * 3 // iota = 3, g = iota * 2, h = iota * 3, g = 6, h = 9i, k // iota = 4, i = iota * 2, k = iota * 3 , i = 8, k = 12
)func main() {// 常量(只读属性)const length int = 10fmt.Println("length = ", length)// 常量不允许修改// length = 100fmt.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() {}()
来声明并调用一个匿名函数
package mainimport "fmt"// 例1 简单的
func foo1(a string, b int) int {fmt.Println("a = ", a)fmt.Println("b = ", b)c := 100return 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 = 1000r2 = 2000return
}// 例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 语言默认使用的是值传递,即在调用过程中不会影响到实际参数。
package mainimport "fmt"func changeValue(p int) {p = 10
}
func main() {var a int = 1changeValue(a)fmt.Println("a =", a) //a= 1
}
引用传递
package mainimport "fmt"func changeValue(p *int) {*p = 10
}
func main() {var a int = 1changeValue(&a)fmt.Println("a =", a)
}
3.5 import导包与init方法
│ main.go
│
├─lib1
│ Lib1.go
│
└─lib2Lib2.go
新版本需要关闭go mod:go env -w GO111MODULE=off
Lib1.go
package lib1import "fmt"// 模块中要导出的函数,必须首字母大写
// 当前lib1包提供的API
func Lib1Test() {fmt.Println("lib1Test()...")
}func init() {fmt.Println("lib1.init() ...")
}
Lib2.go
package lib2import "fmt"// 模块中要导出的函数,必须首字母大写
// 当前lib2包提供的API
func Lib2Test() {fmt.Println("lib2Test()...")
}func init() {fmt.Println("lib2.init() ...")
}
Main.go
package mainimport ("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(先进后出,栈的特性)。
package mainimport "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调用的函数中有效。
package mainimport "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
固定长度的数组
package mainimport "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 = 0fmt.Println("index = ", index, "value = ", value)}// myArray1 types = [10]int// myArray2 types = [6]intfmt.Printf("myArray1 types = %T\n", myArray1)fmt.Printf("myArray2 types = %T\n", myArray2)
}
range会根据遍历的不同的集合来返回不同的值
固定长度的数组在用函数形参接收时,长度也得一样func printArray(myArray [6]int)
,并且是值拷贝
动态数组(切片slice)
切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组")
,
与数组相比,切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
package mainimport "fmt"func printArray(myArray []int) {// 引用传递// _ 表示匿名的变量for _, value := range myArray {fmt.Println("value = ", value)}// 引用传递,修改起作用myArray[0] = 100
}func main() {// 动态数组,切片 slicemyArray := []int{1, 2, 3, 4}// myArray type is []intfmt.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)
package mainimport "fmt"func main() {// 1 - 声明slice1是一个切片,并初始化,默认值是1,2,3。 长度len是3// slice1 := []int{1, 2, 3}// 2 - 声明slice1是一个切片,但没给分配空间。一个切片在未初始化时默认为 nil,长度为 0var 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是否为0if 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)
package mainimport "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 = 5numbers = 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 = 5numbers = 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)
}
切片的截取
package mainimport "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)
}
其他例子
// 使用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
package mainimport ("fmt"
)func main() {// 声明方式1// 声明myMap1是一种map类型 key是string,value是stringvar myMap1 map[string]stringif 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)// 声明方式2myMap2 := make(map[int]string)myMap2[1] = "java"myMap2[2] = "c++"myMap2[3] = "python"fmt.Println(myMap2)// 声明方式3myMap3 := map[string]string{"one": "php","two": "c++","three": "python",}fmt.Println(myMap3)
}
打印结果
myMap1 是一个空map
map[one:java three:python two:c++]
map[1:java 2:c++ 3:python]
map[one:php three:python two:c++]
map基本操作
package mainimport ("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
关键字可以定义一个结构体,结构体对函数参数的传递默认是值传递,只有显示使用指针时,才是引用传递
// 定义一个结构体
type Book struct {title stringauth string
}
方法
package mainimport "fmt"// 声明一种行的数据类型 myint, 是int的一个别名
type myint int// 定义一个结构体
type Book struct {title stringauth string
}func changeBook(book Book) {// 值传递:传递一个book的副本book.auth = "666"
}func changeBook2(book *Book) {// 引用传递:指针传递book.auth = "777"
}func main() {var book1 Bookbook1.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
类型的变量这两个方法都是拥有的。
package mainimport "fmt"// 类名首字母大写,表示其他包也能够访问
type Hero struct {// 类的属性首字母大写, 表示该属性是对外能够访问的,否则的话只能够类的内部访问Name stringAd intlevel 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()
}
类的继承
继承可以重写父类的方法,也可以新增额外的属性和方法
package mainimport "fmt"type Human struct {name stringsex 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 SuperMans.name = "li4"s.sex = "male"s.level = 88s.Eat() // 子类的方法
}
类的多态
Go语言中,多态性只能通过 接口interface 来实现,不能通过 类class 来实现
Go中interface本质是一个指针,会自动指向对应的实现类
一个类实现了某个接口的条件:该类重写了接口中声明的所有方法,则自动实现了该接口
因为接口本质是一个指针,所以在 声明接口变量后,对接口变量进行实现类的赋值,需要赋值的是对象地址
package mainimport "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部分会详细讲解
package mainimport "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使用
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
package mainimport "fmt"func main() {var a string// pair<statictype:string, value:"aceld">a = "aceld"var allType interface{}// pair<type:string, value:"aceld">allType = astr, _ := allType.(string)fmt.Println(str)
}
demo2
package mainimport ("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
package mainimport "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 = br.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()
两种类型(或者说两个方法)可以轻松访问接口变量内容
// 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,示例:
package mainimport ("fmt""reflect"
)func main() {var num float64 = 1.2345// type: float64fmt.Println("type: ", reflect.TypeOf(num))// value: 1.2345fmt.Println("value: ", reflect.ValueOf(num))
}
也就是说明反射可以将“接口类型变量”转换为“反射类型对象”,反射类型指的是reflect.Type和reflect.Value这两种
package mainimport ("fmt""reflect"
)type User struct {Id intName stringAge int
}func (this User) Call() {fmt.Println("user is called ..")fmt.Printf("%v\n", this)
}func DoFiledAndMethod(input interface{}) {// 获取input的typeinputType := reflect.TypeOf(input)fmt.Println("inputType is :", inputType.Name())// 获取input的valueinputValue := reflect.ValueOf(input)fmt.Println("inputValue is:", inputValue)// 通过 type 获取里面的字段// 1. 获取interface的reflect.Type,通过Type得到NumField, 即字段的个数 ,进行遍历// 2. 通过下标 i 得到每个field,数据类型Type,可以获得该Type的 类型 和 字段名// 3. 通过Filed有一个Interface()方法得到 对应的valuefor 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 结构体标签
package mainimport ("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中的应用
package mainimport ("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"}}// 编码的过程 结构体---> jsonjsonStr, 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)
}