文章目录
Go基础学习
Go 包的概念
包的特点
- 不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中
- 包中首字母大写,可被包外访问,首字母小写只能被包成员访问。该规则适用于全局变量、全局常量、类型、结构字段、函数、方法等。
- 包名使用简短小写字母
包引用
- 绝对路径,包的绝对路径就是 $GOROOT/src 或 $GOPATH/src 后面包的源码的全路径(而不是系统根目录/),我们引用标准包都是使用的绝对路径 “xxx”
- 相对路径,当前文件所在目录的相对路径引入包 “./xxx”
- 使用mod管理包,则mod.go所在目录root为根目录, “root/xxx”
- 几种引用格式
import "fmt" // 标准格式 import F "fmt" // 别名 import . "fmt" // 合并fmt的命名空间,可以不用前缀fmt. import _ "fmt" // 只执行包的初始化函数,不使用包 import "github.com/net/http" // 导入网络包,go get会去url上下载这个包
包初始化
初始化顺序
- 根据依赖关系,先初始化未被依赖的包
- 初始化包作用域的变量,不是从上到下的顺序,而是根据依赖关系进行初始化
- 执行init函数
- 每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;
init函数特点
- init函数没有输入参数,没有返回值
Go语言字符串
go语言字符串
go语言直接使用utf8编码,所以不像其他某些语言,需要将文件转成unicode码存储在内存中。而是直接使用utf8码进行操作。
1. 字符串string类型
- string是一个数据类型,是一个常量,只读不能修改,只能转换成[]byte或者[]rune,修改完再转回来。内部实现使用utf8编码,每一个元素类型是uint8。所以对于中文,需要将其转换成rune类型,才能读出一个完整的字符。不能使用&str[0]的方式访问地址。第 i 个字节并不一定是字符串的第 i 个字符,因为对于非 ASCII 字符的 UTF8 编码会要两个或多个字节
- 字符串转换成切片会复制一份内存,所以尽量少转换。
- 字符串是一个2元数据结构,一个指针指向字节数组的起点,另一个表示字节长度。基于字符串创建的切片,依然是字符串,且指向同一个底层字符数组。
- 对字符串进行大量写入时,使用bytes.Buffer类型。 buf.WriteString(“hello”),再使用buf.String()转成字符串。
2. 字节byte和字符rune
- go语言使用byte和rune类型来存储字符。string的存储结构就是[]byte,所以len()是按照byte类型去计算数组长度。使用索引访问byte数组读中文会出现乱码,所以如果想要使用索引的方式读中文,需要转换成rune数组。
- 使用range遍历string时,不再是逐个字节访问,而是会进行utf8解码,保证读一个完整的utf8字符码。也可以理解为是按照字符遍历,字符可能是一个字节,也可能是两个或三个字节。而不像是byte和rune,是固定长度字节遍历。所以字符串可以理解为一个字符数组,它的元素就是字符。
3. 获取字符串长度
最后两种需要转换类型,要进行复制,不建议使用。
strings.Count(str, "") - 1
utf8.RuneCountInString(str)
len([]rune(str))
bytes.Count([]byte(str), nil) - 1
4.字符串和数字转换
- 将数字转换成字符串
fmt.Sprintf("%d", 123) strconv.Itoa(123) strconv.FormatInt(int64(123), 2) // 转换成2进制字符串
- 字符串转换整数
strconv.Atoi("123") strconv.ParseInt("123", 10, 64) //第三个参数是用于指定整型数的大小;16表示 int16,0则表示int。在任何情况下,返回的结果y总是int64类型.
Go语言interface
go语言的接口是实现是解耦合的。当一个结构实现了接口的所有方法,我们就认为这个结构实现了这个接口。原来的c++和java需要先定义接口,再去实现。go可以先实现方法,再去想接口如何定义。
Go语言变量、常量
全局变量
全局变量声明必须以 var 关键字开头
iota特殊常量
- iota 的含义是初始化一个计数器,这个计数器的影响范围只能是 const 后括号作用域范围内的常量。
- iota 不会自动初始化括号作用域内 iota 前边的常量
- iota 默认初始值为 0,我们可以定义的常量初始值从 10 开始
常量组不设置初始值
- 在定义常量组时,如果不提供初始值,则表示将使用上行的表达式值,所以第一个常量值必须要有值
GO语言条件语句
switch
- switch后面的表达式是可选的,如果没有表达式,则case是一个布尔表达式,而不是一个值,相当于多重 if else
- switch 可以是任意支持相等比较运算的类型变量,而不一定是整数
GO语言循环语句
goto
- goto语句只能用于函数内部跳转,只能跳到同级或上级作用域。不能跳过内部变量声明语句
continue
- 和标签一起使用,用户跳出标签所标识的for语句的本次迭代,标签和continue必须在同一个函数内
break
- 和标签一起使用,用于跳出标签所标识的 for 、 switch 、 select 语句的执行,可用于跳出多重循环,但标签和 break 必须在同一个函数内。
GO语言数组
- 申明数组必须指定元素类型以及元素个数,数组的长度是固定的。
数组初始化
- 默认情况下,数组的每个元素会被初始化为元素类型对应的零值
- 不指定长度,根据元素个数设置大小
- 指定长度,通过索引进行初始化。
- 不指定长度,通过索引初始化,数组长度由最后一个索引值确定。
数组长度
- 数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两个不同的数组类型。如果两个数组类型相同,可以使用(== 和 !=)来判断是否相等。
Go语言切片
空切片和nil切片
var s1 []int
var s2 []int{}
s1是声明,还没初始化,是nil值,len和cap是0,array是nil,底层没有分配内存空间
s2是初始化了,不是nil值,是空切片,底层分配了内存空间。更推荐第一种写法
append
var a = []int{1,2,3}
a = append([]int{0}, a...)
a = append([]int{1,2}, a...)
copy
copy(destSlice, srcSlice) int
切片删除元素
Go 语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
a = []int{1,2,3,4}
a = append(a[:i], a[i+n:]...)
GO语言map
map初始化
dict := map[string]int{}
dict := make(map[string]int)
dict := map[string]int{"age":18, "grade": 1}
- map,切片,
访问map
val,ok := map[key]
val := map[key]
第一种采用断言的方式,当 ok 为 true 时,表示 key 存在于字典中,当 ok 为 false 时,表示 key 不存在于字典中;
第二种情况直接查询 key 值的方式,如果 key 不在 map 种时,返回值是字典定义中 value 的类型默认初始值。如 value 的类型为 int,则默认值为 0,如果 value 类型是指针,则默认是 nil;
字典键类型约束
- Go 语言字典的键类型不可以是函数类型、字典类型和切片类型。key 的类型必须支持 == 和 != 两种比较操作符。
- 数组、字符串和结构体都支持 == 和 !=,所以也可以作为字典的key
清空map
Go 语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map ,不用担心垃圾回收的效率, Go 语言中的并行垃圾回收效率比写一个清空函数要高效的多。
map的有序遍历
- 先对key排序,然后再根据key进行遍历
GO语言指针
- go不支持指针的运算,不支持显示指针类型转换。如果非要操作,使用unsafe.Pointer
- 指针被定义后没有分配到任何变量时,它的值为 nil
Go语言结构体
go语言的引用类型为slice, map, chan,函数,接口,指针。引用类型不初始化都是nil
普通实例化
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化。这种用法是为了更明确地表示一个变量被设置为零值。
空结构体
type Empty struct{}
var c = make(chan Empty)
c <- Empty{}
空结构体类型内存占用为0, 通常使用空结构体作为一个事件信息进行Goroutine之间的通信
结构体拥有引用类型
type T struct {
t T // error
}
type T struct {
t *T // ok
st []T // ok
m map[string]T // ok
}
一个类型,它所占用的大小是固定的,因此一个结构体定义好的时候,其大小是固定的。但是,如果结构体里面套结构体,那么在计算该结构体占用大小的时候,就会成死循环。
但如果是指针、切片、map 等类型,其本质都是一个 int 大小(指针,4字节或者8字节,与操作系统有关),即因为指针、map、切片的变量元数据的内存占用大小是固定的,因此该结构体的大小是固定的,类型就能决定内存占用的大小
go语言函数
- go语言没有默认参数值,也不能通过参数指定形参,
- 不支持函数重载
- 不支持命名函数嵌套,但是支持匿名函数嵌套
func add(a, b int) (sum int) { asum := func(x, y int) int { return x + y } return asum(a,b) }
defer
- defer会在return之前执行
package main
func add(x, y int) (z int) {
defer func() {
println(z) // 输出: 203
}()
z = x + y
return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (ret)
}
func main() {
println(add(1, 2)) // 输出: 203
}
- defer函数会被压入runtime实现的defer列表中,会占用内存资源,并且如果for的loop次数很多,这个消耗将很客观。
多个返回值
- 多返回值一般将错误类型作为最后一个返回值,多个返回值只能使用多个变量或"_"忽略###
不定参数
- go支持不定数目的形式参数,格式 param …type
- 类型 …type本质上是一个数组切片
- 不定参数必须是函数的最后一个参数
- 切片作为参数传递给不定参数,切片名后面加上"…"
- 形参为不定参数和切片的函数是不同类型。
- 如果是 …interface{} 则可以是不同类型的可变参数
匿名函数
- 在go语言中,函数是一等公民,函数类型也是一等的数据类型,简单来说,这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。
而更深层次的含义就是:函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。 - 拥有相同的形参和返回值列表,次序,个数和类型都相同的两个函数可以看做是类型相同
GO语言闭包
当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。
闭包记忆效应
被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。
```
package main
import (
“fmt”
)
/*
累加器生成函数,这个函数输出一个初始值,
调用时返回一个为初始值创建的闭包函数
*/
func Accumulate(value int) func() int {
// 返回一个闭包,每次返回会创建一个新的函数实例
return func() int {
// 累加,对引用的 Accumulate 参数变量进行累加,注意 value 不是上一行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
value++
// 返回一个累加值
return value
}
}
func main() {
// 创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
accumulator := Accumulate(1)
// 累加1并打印
fmt.Println(accumulator())
fmt.Println(accumulator())
// 打印累加器的函数地址
fmt.Printf(“%p\n”, &accumulator) //输出 0xc00000e030
// 创建一个累加器, 初始值为10
accumulator2 := Accumulate(10)
// 累加1并打印
fmt.Println(accumulator2())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator2) //输出 0xc00000e040
}
```
可以看出 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。
闭包实现生成器
package main
import (
"fmt"
)
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将 hp 和 name 变量引用到匿名函数中形成闭包。
return name, hp
}
}
func main() {
// 创建一个玩家生成器
generator := playerGen("wohu")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp) // wohu 150
}
go语言异常处理
一个可能造成错误的函数,需要返回一个错误接口error,如果调用是成功的,错误接口将返回nil,否则返回错误。
go语言中没有try/catch捕获异常,提供了两种错误处理方式
- 函数返回error类型对象判断错误
- panic异常
优先使用error,捕获异常。
panic 和 recover
在go中,panic主要有两类来源,一是go运行时,二是开发者通过panic函数主从触发。在panic之前的defer函数依然会被执行。
recover只有在defer中才能生效。当在延迟调用中引发的错误,可被后续的延迟调用捕获,但是仅有最有一个错误可被捕获
package main
import "fmt"
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
Go语言 json
- 默认使用结构体成员名字作为Json的对象,只有导出的结构体成员(首字母大写)才会被编码。
- Color 成员的 Tag 还带了一个额外的 omitempty 选项,表示当 Go 语言结构体成员为空或零值时不生成该 JSON 对象
- json解码时,根据结构的变量名或者tag来对应json的值的。
- json解码时,除了使用结构体接收之外,还可以使用map[string]interface{}来接收。使用map接收,更加灵活
Go语言 文件读写
标准库有os和ioutil
os.Create
func Create(name string) (file *File, err Error)
创建一个文件,如果文件存在则清空全部内容。
os.Open
func Open(name string) (file *File, err Error)
只读模式打开一个文件
os.OpenFile
const (
O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
O_RDWR int = syscall.O_RDWR // 读写模式打开文件
O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
O_CREATE int = syscall.O_CREAT // 如果不存在将创建一个新文件
O_EXCL int = syscall.O_EXCL // 和O_CREATE配合使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
)
func OpenFile(name string, flag int, perm uint32)(file *File, err Error)
flag表示打开模式, perm表示权限模式
写入文件
func (file *File).Write(b []byte) (n int, err Error)
//从 slice 的某个位置开始写入, 返回写的个数, 错误信息,通过 File 的内存地址调用
//要使用该函数,则不能是O_APPEND模式打开
func (file *File).WriteAt(b []byte, off int64) (n int, err Error)
//写入一个字符串, 返回写的个数, 错误信息, 通过 File 的内存地址调用
func (file *File).WriteString(s string) (ret int, err Error)
读取文件
func (file *File).Read(b []byte) (n int, err Error)
func (file *File).ReadAt(b []byte, off int64) (n int, err Error)
删除文件
//传入文件的路径来删除文件,返回错误个数
func Remove(name string) Error
Go语言 goroutine 并发
Go 语言通过编译器运行时( runtime ),从语言上支持了并发的特性。runtime类似java的虚拟机,主要负责内存管理,垃圾回收,栈处理,协成,信道(channel),切片,字典,和反射等。
如上图所示,Go 运行时和用户编译后的代码被 Go 链接器(Linker )静态链接起来,形成一个可执行文件。从运行的角度来说,这个 Go 可执行文件由两部分组成:一部分是用户的代码,另一部分就是 Go 运行时。
goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。
Go语言 竞争、锁资源、同步
1. 原子函数
atomic的原子函数可以原子操作变量,LoadInt64() 和StoreInt64()原子的读写一个整型值。使用其创建一个同步标志。
// 这个示例程序展示如何使用atomic包里的
// Store和Load类函数来提供对数值类型
// 的安全访问
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
var (
shutdown int64
wg sync.WaitGroup
)
func main() {
wg.Add(2)
go doWork("A")
go doWork("B")
time.Sleep(1 * time.Second)
atomic.StoreInt64(&shutdown, 1)
wg.Wait()
}
// doWork用来模拟执行工作的goroutine,
// 检测之前的shutdown标志来决定是否提前终止
func doWork(name string) {
defer wg.Done()
for {
fmt.Printf("Doing %s Work\n", name)
time.Sleep(250 * time.Millisecond)
if atomic.LoadInt64(&shutdown) == 1 {
fmt.Printf("Shutting %s Down\n", name)
break
}
}
}
2. 互斥锁
互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。
package main
import (
"fmt"
"runtime"
"sync"
)
var (
counter int
wg sync.WaitGroup
mutex sync.Mutex
)
// main是所有Go程序的入口
func main() {
wg.Add(2) // 通常都是在一个地方进行add操作然后在不同的协成中进行done
go incCounter(1)
go incCounter(2)
wg.Wait()
fmt.Printf("Final Counter: %d\n", counter)
}
// incCounter使用互斥锁来同步并保证安全访问,
// 增加包里counter变量的值
func incCounter(id int) {
defer wg.Done()
for count := 0; count < 2; count++ {
// 同一时刻只允许一个goroutine进入这个临界区
mutex.Lock()
{ // 使用大括号只是为了让临界区看起来更清晰,并不是必需的。
value := counter
// 当前goroutine从线程退出,并放回到队列
runtime.Gosched()
value++
counter = value
}
mutex.Unlock()
}
}
3. 读写锁RWMutex
读和读之间不影响,写和写,读和写互斥。
var (
count int
countGuard sync.RWMutex
)
func GetCount() int {
countGuard.RLock()
defer countGuard.RUnlock()
return count
}
func SetCount(c int) {
countGuard.Lock()
{
count += c
}
countGuard.Unlock()
}
4. channel
channel也是进行goroutine之间通信的方式。
5. goroutine同步 waitgroup
无缓冲channel和原子操作可以实现goroutine之间的同步,waitgroup也能实现同步,
不要把增加其计数器值的操作和调用其Wait方法的代码,放在不同的 goroutine 中执行。换句话说,要杜绝对同一个WaitGroup 值的两种操作的并发执行。我们最好用 先统一 Add ,再并发 Done ,最后 Wait 这种标准方式,来使用 WaitGroup 值
go语言 channel 通道
- 通道类型的值本身就是并发安全的,这也是go语言自带唯一可以满足并发安全的类型
- 通道的发送和接收都是原子操作
- 发送操作 包括了 复制元素值 和 放置副本到通道内部 这两个步骤。在这两个步骤完全完成之前,发起这个发送操作的那句代码会一直阻塞在那里。
- 接收操作 通常包含了 复制通道内的元素值、放置副本到接收方、 删掉原值 三个步骤,也就是说通常,值进入通道时会被复制一次,然后出通道的时候依照通道内的那个值再被复制一次并给到接收方。在所有这些步骤完全完成之前,发起该操作的代码也会一直阻塞,直到该代码所在的 goroutine 收到了运行时系统的通知并重新获得运行机会为止。
- 无缓冲通道是同步,有缓冲通道是异步操作。
1. 接收channel数据
有两种方式接收channel数据
- data, ok := <-channel 通过ok判断channel是否关闭
- for data := range channel 通道关闭时,会退出循环
- 从已经关闭的通道接收数据,将会接收到通道类型的零值和false,不会阻塞
被关闭的channel不会被设置为nil,尝试对已经关闭的通道进行发送,会触发panic
2. Select 非阻塞通信
Select 中的case条件只能对chan类型变量进行读写操作。
- 在循环中使用select default case 需要避免某个通道被关闭。通道被关闭,就不会再被阻塞,select case就一直处于可用状态。
case value, ok := <-c:
if !ok {
c = make(chan int)
fmt.Println("ch is closed")
} else {7
fmt.Printf("value is %#v\n", value)
}
- 在 select 语句与 for 语句联用时,怎样直接退出外层的 for 语句?
- 可以使用 goto 加 lable 跳转到 for 外面;
- 可以设置一个额外的标记位,当 chan 关闭时,设置 flag=true ,在 for 的最后判断 flag 决定是否 break ;
3. 使用chan实现信号量
go func(id int) {
defer wg.Done()
sem <- 1 // 向 sem 发送数据,阻塞或者成功。
for x := 0; x < 3; x++ {
fmt.Println(id, x)
}
<-sem // 接收数据,使得其他阻塞 goroutine 可以发送数据。
}(i)
Go语言 type类型
type的作用,1)定义结构体,2)定义接口,3)定义类型,4)类型别名,5)类型查询
1. 类型别名和新类型
type TypeAlias = Type
type newType Type
- 类型别名和对应类型属于同一个类型,能够相互赋值和运算。但是类型定义是一种新的类型,本身依然具备Type类型的特性,新类型和底层类型不能相互直接赋值和运算,需要显示转换。
- 不能在一个非本地(同一个包)的类型上定义新方法。
- 给类型别名新增方法,会添加到原类型方法集中
2. type类型查询
类型查询只能在switch中对interface{}生效,
switch t := a.(type) {
case string:
fmt.Println("字符串")
case int:
fmt.Println("字符串")
default :
fmt.Println("其他类型", v)
}
3. nil值
- nil值不能相互比较,不同类型的nil值不能比较,相同类型中 function,slice,map类型的nil值也不能比较
- nil 是function,slice,map,chan,pointer,interface的零值
go语言 range
参与循环的是range表达式的副本
func main() {
a := [3]int{0, 1, 2}
a := []int{0,1,2}
for i, v := range a { // index、value 都是从复制品中取出。
if i == 0 { // 在修改前,我们先修改原数组。
a[1], a[2] = 999, 999
fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
}
a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。
}
fmt.Println(a) // 输出 [100, 101, 102]。
}
rang a使用的是副本,因为数组类型副本是值拷贝,所以循环内的修改是修改的数组副本。但是slice是引用,副本就是引用本身。所以循环内的修改会影响到外面的切片本身
go语言 make和new的区别
- make 只能用来分配及初始化类型为 slice 、 map 、 chan 的数据。 new 可以分配任意类型的数据;
- new 分配返回的是指针,即类型 *Type 。 make 返回引用,即 Type ;
- new 分配的空间被清零。 make 分配空间后,会进行初始化;
Go语言结构体
内嵌结构体
- 内嵌的结构体可以直接访问期成员变量
- 内嵌结构体的字段名是他的类型名字
Go语言数据类型
底层类型
所有的类型都有一个底层类型,
- 简单类型和复合类型的底层类型是它们自身
- 自定义类型 type newtype oldtype 中 newtype 的底层类型是逐层递归向下查找的,直到查到的 oldtype 是简单类型或复合类型为止。
type T1 string
type T3 []string
type T5 []T1
T5 与 T3 、 的底层类型是不一样的, 一个是 []T1 ,另一个是 []string
相同类型和类型赋值
Go语言方法
1. 方法的特点
- Go语言对receiver参数的基类型不能使指针类型和接口类型
- 为类型增加方法有一个限制,就是方法的定义必须和类型的定义在同一个包中。所以我们不能给原生类型添加方法(int,map等)
- 使用 type 定义的自定义类型是一个新类型,新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承。
2. 方法值
f := x.M
f(args...)
// 等价于
x.M(args...)
变量 x 的静态类型是 T , M 是类型 T 的一个方法, x.M 被称为方法值。方法值是一个带有闭包的函数变量。
Go语言接口
- 接口定义大括号内可以是方法声明的集合,也可以嵌入另一个接口类型匿名字段,还可以是二者的混合。
- Go 1.14 版本以后,Go 接口类型允许嵌入的不同接口类型有相同的方法,并且方法的函数签名也一样(也就是函数名,参数,返回值等保持一致)
- 接口名一般以"er"结尾,函数内部方法名不需要以func引导,接口只有申明没有实现
- 惯例上尽量定义小接口。
- 只能给自定义类型定义方法。非命名类型由于不能定义自己的方法, 所以方法集为空,因此其类型变量除了传递给空接口,不能传递给任何其他接口
1. 类型断言
var i interface
i.(T)
func main() {
a := 1
v, ok := interface{}(a).(int) // 将 a 转换为接口类型
if ok {
fmt.Printf("v type is %T\n", v)
}
fmt.Println(a)
}
- i必须是接口变量, T可以是接口类型名,也可以是具体类型名
- 如果 TypeNname 是一个具体类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否就是具体类型 TypeNname
- 如果 TypeName 是一个接口类型名,则类型断言用于判断接口变量 i 绑定的实例类型是否同时实现了 TypeName 接口
2. 类型赋值
o := i.(TypeName)
- TypeName 是具体类型名,此时如果接口 i 绑定的实例类型就是具体类型 TypeName ,则变量 o 的类型就是 TypeName , 变量 o 的值就是接口绑定的实例值的副本(当然实例可能是指针值,那就是指针值的副本) 。
- TypeName 是接口类型名, 如果接口 i 绑定的实例类型满足接口类型 TypeName ,则变量 o 的类型就是接口类型TypeName , o 底层绑定的具体类型实例是 i 绑定的实例的副本(当然实例可能是指针值,那就是指针值的副本〉。
3. comma,ok表达式
如果类型断言的结果为否,就意味着该类型断言是失败的,失败的类型断言会引发 panic(运行时异常),解决方法
var i1, ok := interface{}(i).(TypeNname)
其中 ok 值体现了类型断言的成败,如果成功,i1 就会是经过类型转换后的 TypeNname 类型的值,否则它将会是 TypeNname 类型的零值(或称为默认值)
4. 类型查询
func main() {
var x interface{} = 13
switch v := x.(type) {
case nil:
println("v is nil")
case int:
println("the type of v is int, v =", v)
case string:
println("the type of v is string, v =", v)
case bool:
println("the type of v is bool, v =", v)
default:
println("don't support the type")
}
}
v 存储的是变量 x 的动态类型对应的值信息(而不是类型信息),这样我们在接下来的 case 执行路径中就可以使用变量 v 中的值信息了。
5. 空接口
type Inter interface {
Ping()
Pang()
}
type St struct{}
func (St) Ping() {
println("ping")
}
func (*St) Pang() {
println("pang")
}
func main() {
var st *St = nil
var it Inter = st
fmt.Printf("%p\n", st) // 0x0
fmt.Printf("%p\n", it) // 0x0
if it != nil {
it.Pang() // pang
// 下面的语句会导致panic
// 方法转换为函数调用,第一个参数是St 类型,由于 *St 是nil ,无法获取指针所指的对象值,所以导致panic
// it.Ping()
}
}
这个程序暴露出 Go 语言的一点瑕疵, fmt.Printf(“%p\n”, it) 的结果是 0x0 ,但 it! = nil 的判断结果却是 true,
空接口有两个字段, 一个是实例类型, 另一个是指向绑定实例的指针,只有两个都为 nil 时,空接口才为 nil 。上述中,it的指针为nil,而实例类型不为nil。
Go语言 基于Go方法的面向对象(封装、继承、多态)
Go 语言的结构体(struct)和其他语言的类(class)有同等的地位,但 Go 语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。
1. 封装
封装其实就是创建一个struct,并实现一些方法。
2. 继承
确切地说,Go 语言也提供了继承,但是采用了组合的文法,所以我们将其称为匿名组合,Go 语言的继承方式采用的是匿名组合的方式。
package main
type Base struct {
Name string
}
func (base *Base) Foo() { ... }
func (base *Base) Bar() { ... }
type Foo struct {
Base
...
}
func (foo *Foo) Bar() {
foo.Base.Bar()
...
}
上述Foo继承了Base的两个函数,然后从Base类继承并改写了Bar()方法。
2.1 名字冲突
type X struct {
Name string
}
type Y struct {
X
Name string
}
所有的 Y 类型的 Name 成员的访问都只会访问到最外层的那个 Name 变量,X.Name 变量相当于被隐藏起来了。
3. 多态
在 Go 语言中可以使用接口实现这一特征。
Go语言反射
1. 反射的概念
反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时,变量被转换为内存地址,变量名不会被编译器写入到可执行部分。在运行程序时,程序无法获取自身的信息。
支持反射的语言可以在程序编译期将变量的反射信息,如字段名称、类型信息、结构体信息等整合到可执行文件中,并给程序提供接口访问反射信息,这样就可以在程序运行期获取类型的反射信息,并且有能力修改它们
Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成,并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
2. 反射的类型对象 reflect.TypeOf
2.1 反射的类型(Type)和种类(Kind)
- Go语言程序中的类型(Type)指的是系统原生数据类型,如 int、string、bool、float32 等类型,以及使用 type 关键字定义的类型,这些类型的名称就是其类型本身的名称
- 种类(Kind)指的是对象归属的品种,在 reflect 包中有如下定义:Map、Slice、Chan 属于引用类型,使用起来类似于指针,但是在种类常量定义中仍然属于独立的种类,不属于 Ptr。type A struct{} 定义的结构体属于 Struct 种类,*A 属于 Ptr。
type Kind uint
const (
Invalid Kind = iota // 非法类型
Bool // 布尔型
Int // 有符号整型
Int8 // 有符号8位整型
Int16 // 有符号16位整型
Int32 // 有符号32位整型
Int64 // 有符号64位整型
Uint // 无符号整型
Uint8 // 无符号8位整型
Uint16 // 无符号16位整型
Uint32 // 无符号32位整型
Uint64 // 无符号64位整型
Uintptr // 指针
Float32 // 单精度浮点数
Float64 // 双精度浮点数
Complex64 // 64位复数类型
Complex128 // 128位复数类型
Array // 数组
Chan // 通道
Func // 函数
Interface // 接口
Map // 映射
Ptr // 指针
Slice // 切片
String // 字符串
Struct // 结构体
UnsafePointer // 底层指针
)
3. 指针和指针指向的元素
Go语言程序中对指针获取反射对象时,可以通过 reflect.Elem() 方法获取这个指针指向的元素类型,这个获取过程被称为取元素,等效于对指针类型变量做了一个*操作
4. 使用反射获取结构体的成员类型
结构体通过 reflect.TypeOf() 获得反射对象信息后,可以通过反射值对象的 NumField() 和 Field() 方法获得结构体成员的详细信息。
方法 | 说明 |
---|---|
Field(i int) StructField | 根据索引返回索引对应的结构体字段的信息,当值不是结构体或索引超界时发生宕机 |
NumField() int | 返回结构体成员字段数量,当类型不是结构体或索引超界时发生宕机 |
FieldByName(name string) (StructField, bool) | 根据给定字符串返回字符串对应的结构体字段的信息,没有找到时 bool 返回 false,当类型不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) StructField | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的信息,没有找到时返回零值。当类型不是结构体或索引超界时发生宕机 |
FieldByNameFunc(match func(string) bool) (StructField,bool) | 根据匹配函数匹配需要的字段,当值不是结构体或索引超界时发生宕机 |
1) 结构体字段类型
reflect.Type 的 Field() 方法返回 StructField 结构,这个结构描述结构体的成员信息,通过这个信息可以获取成员与结构体的关系,如偏移、索引、是否为匿名字段、结构体标签(StructTag)等,而且还可以通过 StructField 的 Type 字段进一步获取结构体成员的类型信息。
type StructField struct {
Name string // 字段名
PkgPath string // 字段路径
Type Type // 字段反射类型对象
Tag StructTag // 字段的结构体标签
Offset uintptr // 字段在结构体中的相对偏移
Index []int // Type.FieldByIndex中的返回的索引值
Anonymous bool // 是否为匿名字段
}
字段说明如下:
- Name:为字段名称。
- PkgPath:字段在结构体中的路径。
- Type:字段本身的反射类型对象,类型为 reflect.Type,可以进一步获取字段的类型信息。
- Tag:结构体标签,为结构体字段标签的额外信息,可以单独提取。
- Index:FieldByIndex 中的索引顺序。
- Anonymous:表示该字段是否为匿名字段。
5. 获取结构体成员的值
reflect.Value 可以通过函数 reflect.ValueOf 获得,reflect.Value 被定义为一个 struct 结构体:
type Value struct {
type *rtype
ptr unsafe.Pointer
flag
6. 反射值的方法
6.1 通过反射获取值的方法
方法名 | 说明 |
---|---|
Interface() interface {} | 将值以 interface{} 类型返回,可以通过类型断言转换为指定类型 |
Int() int64 | 将值以 int 类型返回,所有有符号整型均可以此方式返回 |
Uint() uint64 | 将值以 uint 类型返回,所有无符号整型均可以此方式返回 |
Float() float64 | 将值以双精度(float64)类型返回,所有浮点数(float32、float64)均可以此方式返回 |
Bool() bool | 将值以 bool 类型返回 |
Bytes() []bytes | 将值以字节数组 []bytes 类型返回 |
String() string | 将值以字符串类型返回 |
6.2 通过反射访问结构体成员值的方法
射值对象(reflect.Value)提供对结构体访问的方法,通过这些方法可以完成对结构体任意值的访问
方法 | 说明 |
---|---|
Field(i int) Value | 根据索引,返回索引对应的结构体成员字段的反射值对象。当值不是结构体或索引超界时发生宕机 |
NumField() int | 返回结构体成员字段数量。当值不是结构体或索引超界时发生宕机 |
FieldByName(name string) Value | 根据给定字符串返回字符串对应的结构体字段。没有找到时返回零值,当值不是结构体或索引超界时发生宕机 |
FieldByIndex(index []int) Value | 多层成员访问时,根据 []int 提供的每个结构体的字段索引,返回字段的值。 没有找到时返回零值,当值不是结构体或索引超界时发生宕机 |
FieldByNameFunc(match func(string) bool) Value | 根据匹配函数匹配需要的字段。找到时返回零值,当值不是结构体或索引超界时发生宕机 |
6.3 通过反射修改变量值
要修改一个变量的值,有几个关键点:传递指针(可寻址),通过 Elem 方法获取指向的值,才可以保证值可以被修改,reflect.Value 为我们提供了 CanSet 方法判断是否可以修改该变量。
func main() {
s := "hello"
sv := reflect.ValueOf(&s) // 必须是&s,否则报错
// panic: reflect: call of reflect.Value.Elem on string Value
sv.Elem().SetString("world")
fmt.Println(s) // world
}
那么如何修改 struct 结构体字段的值呢?可总结出以下步骤:
- 传递一个 struct 结构体的指针,获取对应的 reflect.Value ;
- 通过 Elem 方法获取指针指向的值;
- 通过 Field 方法获取要修改的字段;
- 通过 Set 系列方法修改成对应的值。
func main() {
p := person{Name: "wohu", Age: 18}
ppv := reflect.ValueOf(&p)
ppv.Elem().Field(0).SetString("张三")
fmt.Println(p) // {张三 18}
}
type person struct {
Name string
Age int
}
最后再来总结一下通过反射修改一个值的规则。
- 可被寻址,通俗地讲就是要向 reflect.ValueOf 函数传递一个指针作为参数。
- 如果要修改 struct 结构体字段值的话,该字段需要是可导出的,而不是私有的,也就是该字段的首字母为大写。
- 记得使用 Elem 方法获得指针指向的值,这样才能调用 Set 系列方法进行修改。