Go语言圣经
Go语言圣经
入门
- Go是一门编译型语言,静态编译
- go run helloworld.go
- go build helldworld.go
- Go语言编译过程没有警告信息
- Go语言的设计包含的诸多的安全策略,编译时类型检查检查可以发现大多数类型不匹配的操作
程序结构
- 声明:var/const/type/func
- Go语言中不存在没有初始化的变量
- 简短变量声明用于函数内部
- 声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该变量的指针p,p指针保存了x变量的内存地址,*p表达式对应p指针指向的变量的值
- *p++只是增加p指向的变量的值,并不改变p指针
- new(int)创建一个*int类型的指针变量
- 比较运算符==和<也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较
- 对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
- 这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用
- 初始化自下而上进行,确保main函数执行之前,所有包都已完成初始化
- 作用域和生命周期
- 作用域
名字的作用域是程序文本的一部分,名字在其中可见。即你什么时候可以看见它并找到它 - 生命周期
指的是程序执行过程中该对象存在的一段时间
- 作用域
基础数据类型
- 整型
- 有符号整型:int8/int16/int32/int64
- 无符号整型:uint8/uint16/uint32/uint64
- 取模运算符%仅用于整数之间的运算,取模运算符的符号和被取模数的符号一致
- 字符串处理
- bytes:针对和字符串有相同结构的[]byte类型
- strings:字符串的查询、替换、比较、截断、拆分和合并等功能
- strconv:提供了布尔型、整型数、浮点数和对应字符串的相互转换
- unicode:提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类
复合数据类型
-
数组
- 定长
- 数组的长度是数组类型的一个组成部分
-
切片(Slice)
-
make([]T,len,cap)
-
变长的序列,序列中元素类型相同
-
组成部分:指针、长度和容量
- 指针指向第一个slice元素对应的底层数组元素的地址
- 第一个元素不一定是数组的第一个元素
- 长度(len)对应slice中元素的树目
- 容量(cap)是从slice的开始位置到底层数据结构的结尾位置
- 指针指向第一个slice元素对应的底层数组元素的地址
-
多个slice之间可以共享底层的数据,并且引用可以重叠
-
切片操作超出cap的上限会导致一个panic,超出len意味扩展了slice
-
slice之间不能直接比较,但对于字节型可以使用bytes.Equal函数判断
- slice元素间接引用,一个slice甚至可以包含自身
- 一个slice不同时间可能包含不同的值
-
append函数(对于理解slice底层如何工作十分重要)
-
-
Map
- make(map[string]int)
- ok:可用于在map中判断0值是否存在
- 将value设置为bool可以实现set
-
结构体
- 聚合的数据类型(由任意类型)
- 直接赋值 or 通过指针访问
- 调用函数返回的是值,而不是一个可取地址的变量,返回结构体指针对结构体的属性进行修改
- 一个聚合的值不能包含它自身,但是S类型的结构体可以包含*S指针类型的成员。可用于创建递归的数据结构(链表和树)
- 结构体的面值
- 根据属性顺序赋值
- 以成员名字和相应的值来初始化,成员被忽略默认使用零值
- 结构体比较
- 如果结构体的全部成员都可比较,那么结构体也可以比较(== or !=)
- 结构体可以用于map的key
- 结构体嵌入和匿名成员
- 结构体嵌入,类似组合模式
- 匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针
- 好处
- 可以直接访问叶子属性而不需要给出完整的路径
- 可以将一个有简单行为的对象 组合成有复杂行为的对象
- 缺点
- 字面值没有简短表示匿名成员的语法
- 不能包含两个类型相同的匿名成员
- 好处
-
Json
- JavaScript对象表示法 (Json)是一种用于发送和接收结构化信息的标准协议
- 可以用有效可读的方式表示基础数据类型、数组、slice、map和结构体
- Json的类型:数字、布尔值和字符串
- 将结构体转为Json的过程叫编组(json.Marshal())
- json.MarshalIndent()使用缩进将输出格式化,便于阅读
- 编码的逆操作是解码,使用json.Unmarshal(),可以选择成员进行解码
- 从一个输入流解码Json数据json.Decoder()
- 将数据编码为json对象写入输出流json.Encoder()
-
文本和HTML模板
函数
函数可以把一个大工作分解为小的任务,可以多次调用
-
函数声明
- 函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体
- 形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由参数调用者提供
- 返回值列表描述了函数返回值的变量名以及类型。如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的
- 返回值也可以像形式参数一样被命名,这样的话返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0
- 如果两个函数形参列表和返回值列表中的变量类型一一对应,那么它们有相同的类型和标识符
- 每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义
- 在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中
- 实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会被修改
- 函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体
-
递归
- 函数可以直接和间接的调用自身
-
多返回值
- Go中,一个函数可以有多个返回值,可以给返回值命名
- 函数可以作为返回值和参数
- 如果一个函数将所有返回值都显示的变量名,那么该函数的return语句可以省略操作数
-
错误
- 运行失败看作是预期结果的函数,它们会返回一个额外的返回值来传递错误信息
- error接口类型
- nil:函数运行成功
- non-nil:函数运行失败
- 当函数返回non-nil的error时,其他未定义的返回值应该被忽略
- Go语言中,函数运行失败会返回错误(预期的值而非异常),有别于将函数运行失败看作异常的语言。Go的各种异常机制仅用于处理未被预料的错误
- 错误处理策略(调用函数返回错误时)
- 传播错误:函数中某个子程序的失败,会变成函数的失败
- 重新尝试失败的操作:如果错误的发生是偶然性的,或由不可预知的问题导致的
- 输出错误信息并结束程序(只应在main中执行):错误发生后,程序无法继续运行。log中的所有函数,都默认会在错误信息之前输出时间信息
- 有时只需要输出错误信息,不需要中断程序的运行。我们可以通过log包提供函数或者标准错误流输出错误信息
- 我们可以直接忽略掉错误
- 文件结尾错误(EOF)
- 由于读取文件失败的不同场景,io包保证文件结束引起的读取失败都返回同一个错误
-
函数值
- Go中,函数被看作第一类值
- 拥有类型
- 可以被赋值给其他变量
- 传递给函数
- 从函数返回
- 函数类型的零值是nil,调用值为nil的函数值会引起panic错误
- 函数值可以和nil比较,但是函数值之间不可以比较,也不能作为map的key
- 函数值使我们不仅仅可以通过数据来参数化函数,亦可通过行为
- Go中,函数被看作第一类值
-
匿名函数
-
拥有函数名的函数只能在包级语法块中被声明
-
函数字面量可以在使用函数时,再定义它
-
在函数中定义的内部函数可以引用该函数的变量
// squares返回一个匿名函数。 // 该匿名函数每次被调用时都会返回下一个数的平方。 func squares() func() int { var x int return func() int { x++ return x * x } } func main() { f := squares() fmt.Println(f()) // "1" fmt.Println(f()) // "4" fmt.Println(f()) // "9" fmt.Println(f()) // "16" }
- 例子证明,函数值记录了状态。在squares中定义的匿名内部函数可以访问和更新squares中的局部变量。这就是函数值属于引用类型和函数值不可比较的原因。我们看到变量的生命周期不由它的作用域决定,squares返回后,变量x仍然隐式的存在于f中
-
-
可变参数
func sum(vals...int) int { total := 0 for _,val := range vals { total += val } return total } fmt.Println(sum()) fmt.Println(sum(1,2))
方法
-
方法声明
package main import "fmt" type rect struct { width, height int } func (r *rect) area() int { return r.width * r.height } func (r rect) perim() int { return 2*r.width + 2*r.height } func main() { r := rect{width: 10, height: 5} fmt.Println("area: ", r.area()) fmt.Println("perim:", r.perim()) rp := &r fmt.Println("area: ", rp.area()) fmt.Println("perim:", rp.perim()) }
- Go语言的方法可以给一些简单的数值、字符串、slice、map定义一些附加的行为。方法可以声明到任意类型,只要不是一个指针或者interface
-
基于指针对象的方法
-
当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数过大我们希望可以避免这种默认拷贝,这种情况需要指针
-
类型本身是指针不可以做为接收器
-
如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器
p := Point{1,2} //两种一样,编译器隐式帮我们用&p调用方法 --(&p).ScaleBy(2) --p.ScaleBy(2) //false,为指针时不可以用临时变量,无地址 --Point{1,2}.ScaleBy(2)
-
il也是一个合法的接收器类型
-
-
方法值和方法表达式
distanceFromP := p.Distance fmt.Println(distanceFromP(q))
-
封装
- Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义他们的包中被导出,小写字母的则不会。这种限制包内成员的方法同样适用于struct或者一个类型的方法。因而如果我们想要一个封装一个对象,我们必须将其定义为一个struct
接口
-
Go语言的接口满足隐式实现
- 没有必要对于给定的具体类型定义所有满足的接口类型
- 创建新接口类型满足已经存在的具体类型却不会去改变这些类型的定义
-
一个类型如果拥有一个接口需要的全部方法,那么这个类型就实现了这个接口
-
类型断言(x.(T))
-
转化类型
var t int var x interface{} x = t //转成int y, ok = x.(int)
-
判断一个变量是否实现了指定的接口
type test interface { run() } type tet struct{} func (t tet) run() { fmt.Println("go go go") } func main() { var t interface{} t = tet{} if test, ok := t.(test); ok { //输出go go go test.run() } }
-
Goroutines和Channels
Goroutines
-
go语句会使其语句中的函数在一个单独的goroutine中运行。而go语句本身会迅速的完成
f() //wait for it return go g() //don't wait return
-
go语言内置了调度和上下文切换的机制,智能的将goroutine中的任务合理地分配给每个CPU
-
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数
-
程序启动时,Go程序会为main()函数创建一个默认的goroutine,当main()函数返回的时候其他的goroutine都会结束,可以使用time.Sleep(time.Second)等待
func hello() { fmt.Println("hello") } func main() { go hello() fmt.Println("world") //time.Sleep(time.Second) }
Channels
-
goroutine之间的通信机制,每个channel都有一个特殊的类型,也就是channels可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int
-
创建channel(零值为nil)
//创建无缓存的channel ch := make(chan int) ch := make(chan int, 0) //创建有缓存的channel ch := make(chan int, 3)
-
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相通的对象,那么比较的结果为真。一个channel也可以和nil进行比较
-
channel的基本使用
ch <- x x = <-ch <-ch close(ch)
-
无缓存的channel(同步Channels)
- 发送会导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作;接收操作先发生也会阻塞
- 并发编程
- x事件发生在y事件之前,不是说x发生的比y早,而是y发生之前x已经结束
- x事件和y事件是并发的,不一定是同时发生,只是我们不能确定事件发生的先后顺序
-
串联的Channels(Pipeline)
-
版本1
func main() { naturals := make(chan int) squares := make(chan int) //Counter go func() { for x := 0; ; x++ { naturals <- x } }() //Squarer go func() { defer close(squares) for { x, ok := <-naturals if !ok { break } squares <- x * x } }() for { fmt.Println(<-squares) } }
-
版本2
func main() { naturals := make(chan int) squares := make(chan int) //Counter go func() { defer close(naturals) for x := 0; x < 100; x++ { naturals <- x } }() //Squarer go func() { defer close(squares) for x := range naturals { squares <- x * x } }() for x := range squares { fmt.Println(x) } }
-
-
单向的channel
-
当一个channel作为一个函数参数是,它一般总是被专门用于只发送或者只接收
chan<-int:表示一个只发送int的channel <-chan int:表示
-
因为关闭操作只用于断言不再向channel发送新的数据,所以只有在发送者所在的goroutine才会调用close函数,因此对一个只接收的channel调用close将是一个编译错误
-
双向型channel可以隐式的转为单向的channel,不可反向转换
func counter(out chan<- int) { for x := 0; x < 24; x++ { out <- x } close(out) } func squarer(out chan<- int, in <-chan int) { for x := range in { out <- x * x } close(out) } func print(in <-chan int) { for x := range in { fmt.Println(x) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) print(squares) }
-
-
带缓存的Channels
-
带缓存的Channel内部持有一个元素队列。队列的最大容量是在调用make函数创建channel时通过第二个参数指定的
ch = make(chan string, 3)
-
向缓存Channel的发送操作就是向内部缓存队列的尾部插入原因,接收操作则是从队列的头部删除元素
-
func mirroredQuery() string { responses := make(chan string, 3) go func() { responses <- request("asia.gopl.io") }() go func() { responses <- request("europe.gopl.io") }() go func() { responses <- request("americas.gopl.io") }() return <-responses // return the quickest response } func request(hostname string) (response string) { /* ... */ }
- 最后只返回了响应最快的结果;如果使用无缓存的channel,那么两个慢的goroutines将会因为没有人接收而被永远卡住。造成goroutines泄漏。和垃圾变量不同,泄漏的goroutines并不会被自动回收,因此要确保每个不再需要的goroutine能正常退出
-
-
select多路复用(类似switch)
-
可以同时从多个通道并行读取
func main() { //定义一个管道10个数据int intChan := make(chan int, 10) for i := 0; i < 10; i++ { intChan <- i } //定义一个管道5个数据string stringChan := make(chan string, 5) for i := 0; i < 5; i++ { stringChan <- "hello" + fmt.Sprintf("%d", i) } for { select { case v := <-intChan: fmt.Printf("从 intChan 读取的数据%d\n", v) case v := <-stringChan: fmt.Printf("从 stringChan 读取的数据%v\n", v) default: fmt.Printf("数据获取完毕") return } } }
-
基于共享变量的并发
-
银行存储款并发安全版(不加锁),使用通信来共享数据
var ch1 = make(chan int) var ch2 = make(chan int) func deposity(amount int) { ch1 <- amount } func balance() int { return <-ch2 } func teller() { var balance int for { select { case v := <-ch1: balance += v case ch2 <- balance: } } } func main() { go deposity(123) go deposity(123) go teller() time.Sleep(time.Second) fmt.Println(balance()) }
-
sync.Mutex互斥锁(一个只能为1和0的信号量叫做二元信号量sema)
-
模拟信号量
var ( sema = make(chan struct{}, 1) balance int ) func Deposit(amount int) { sema <- struct{}{} balance = balance + amount <-sema } func Balance() int { sema <- struct{}{} b := balance <-sema return b } func main() { go Deposit(123) go Deposit(123) time.Sleep(time.Second) fmt.Println(Balance()) }
-
sync包使用
var ( mu sync.Mutex balance int ) func Deposit(amount int) { mu.Lock() defer mu.Unlock() balance = balance + amount } func Balance() int { mu.Lock() defer mu.Unlock()//return后被调用 return balance } func main() { go Deposit(123) go Deposit(123) time.Sleep(time.Second) fmt.Println(Balance()) }
-
-
go语言为什么不支持可重入锁?
-
互斥量的目的是为了确保共享变量在程序执行时的关键点上能够保证不变性。不变性的其中之一是“没有goroutine访问共享变量”。但实际上对于mutex保护的变量来说,不变性还包括其它方面。当一个goroutine获得了一个互斥锁时,它会断定这种不变性能够被保持。其获取并保持锁期间,可能会去更新共享变量,这样不变性只是短暂地被破坏。然而当其释放锁之后,它必须保证不变性已经恢复原样。尽管一个可以重入的mutex也可以保证没有其它的goroutine在访问共享变量,但这种方式没法保证这些变量额外的不变性
func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() Deposit(-amount)//Deposit内部会再次去尝试给mu加锁,错误 if Balance() < 0 { Deposit(amount) return false // insufficient funds } return true }
-
解决方法
var ( mu sync.Mutex balance = 456 ) func Withdraw(amount int) bool { mu.Lock() defer mu.Unlock() deposit(-amount) if Balance() < 0 { deposit(amount) return false // insufficient funds } return true } func Deposit(amount int) { mu.Lock() defer mu.Unlock() deposit(amount) } func Balance() int { return balance } func deposit(amount int) { balance = balance + amount } func main() { go Withdraw(123) time.Sleep(time.Second) fmt.Println(Balance()) }
-
-
sync.RWMutex读写锁(多个读并发,写操作互斥)
-
RLock和RUnlock方法
var mu sync.RWMutex var balance int func Balance() int { mu.RLock() // readers lock defer mu.RUnlock() return balance }
- RLock只能在临界区共享变量没有任何写入操作时可用
-
-
内存同步
- 同一个goroutine顺序执行,不同goroutine并发执行
- 将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问
-
sync.Once初始化
-
初始化代码(初次初始化用排他锁,初始化后用读写锁)
- goroutine首先会获取一个写锁,查询map,然后释放锁。如果找到就直接返回。如果没有找到,那goroutine会获取一个写锁。不释放共享锁的话,也没有任何办法来将一个共享锁升级为一个互斥锁,所以我们必须重新检查icons变量是否为nil,以防止在执行这一段代码的时候,icons变量已经被其它gorouine初始化过了
var mu sync.RWMutex // guards icons var icons map[string]string // Concurrency-safe. func Icon(name string) string{ mu.RLock() if icons != nil { icon := icons[name] mu.RUnlock() return icon } mu.RUnlock() // acquire an exclusive lock mu.Lock() if icons == nil { // NOTE: must recheck for nil loadIcons() } icon := icons[name] mu.Unlock() return icon }
-
go语言提供了专门的方案来解决一次性初始化的问题
var loadIconsOnce sync.Once var icons map[string]image.Image // Concurrency-safe. func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] }
- 每一次对Do(loadIcons)的调用都会锁定mutex,并会检查boolean变量。在第一次调用时,变量的值是false,Do会调用loadIcons并会将boolean设置为true。随后的调用什么都不会做,但是mutex同步会保证loadIcons对内存(这里其实就是指icons变量啦)产生的效果能够对所有goroutine可见。用这种方式来使用sync.Once的话,我们能够避免在变量被构建完成之前和其它goroutine共享该变量
-
-
竞争条件检测
- 只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock,(*sync.WaitGroup).Wait等等的调用
- 只能检测到运行时的竞争条件
-
Goroutines和线程
-
动态栈
- 每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量,固定内存会导致空间浪费大
-
Goroutine调度
- OS线程会被操作系统内核调度。每几毫秒,一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。这个函数会挂起当前执行的线程并保存内存中它的寄存器内容,检查线程列表并决定下一次哪个线程可以被运行,并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程(上下文切换)
- Go的运行时包含了其自己的调度器,这个调度器使用了一些技术手段,比如m:n调度,因为其会在n个操作系统线程上多工(调度)m个goroutine。Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine(不需要进行上下文切换)
-
GOMAXPROCS
-
Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。其默认的值是运行机器上的CPU的核心数
-
在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计数在内
-
在第一次执行时,最多同时只能有一个goroutine被执行。初始情况下只有main goroutine被执行,所以会打印很多1。过了一段时间后,GO调度器会将其置为休眠,并唤醒另一个goroutine,这时候就开始打印很多0了,在打印的时候,goroutine是被调度到操作系统线程上的。在第二次执行时,我们使用了两个操作系统线程,所以两个goroutine可以一起被执行,以同样的频率交替打印0和1
for { go fmt.Print(0) fmt.Print(1) } $ GOMAXPROCS=1 go run main.go 111111111111111111110000000000000000000011111... $ GOMAXPROCS=2 go run main.go 010101010101010101011001100101011010010100110...
-
-
Goroutine没有ID号
- 在大多数支持多线程的操作系统和程序语言中,当前的线程都有一个独特的身份(id),并且这个身份信息可以以一个普通值的形式被被很容易地获取到。这种情况下我们做一个抽象化的thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突
- goroutine没有可以被程序员获取到的身份(id)的概念。这是Golang的开发者故意为之,避免开发者滥用Goroutine Id实现Goroutine Local Storage(类似java的Thread Local Storage), 因为Goroutine Local Storage很难进行垃圾回收。因此尽管Go1.4之前暴露出了相应的方法,现在已经把它隐藏了
- 对于高并发日志的查看和过滤就变得比较困难。尽管在日志中可以使用业务本身的Id
-
反射
-
Go提供了一种机制在运行时更新变量和检查它们的值, 调用它们的方法, 和它们支持的内在操作, 但是在编译时并不知道这些变量的类型. 这种机制被称为反射
-
为什么需要反射?
- 有时候我们需要编写一个函数能够处理一类并不满足普通公共接口的类型的值,
- 也可能它们并没有确定的表示方式,
- 或者在我们设计该函数的时候还这些类型可能还不存在
-
reflect.Type和reflect.Value
-
函数 reflect.TypeOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Type
t := reflect.TypeOf(3) // a reflect.Type fmt.Println(t.String()) // "int" fmt.Println(t) // "int" fmt.Printf("%T\n", 3) // "int"
-
reflect 包中另一个重要的类型是 Value. 一个 reflect.Value 可以持有一个任意类型的值. 函数reflect.ValueOf 接受任意的 interface{} 类型, 并返回对应动态类型的reflect.Value. 和reflect.TypeOf 类似,reflect.ValueOf 返回的结果也是对于具体的类型, 但是 reflect.Value 也可以持有一个接口值
v := reflect.ValueOf(3) // a reflect.Value fmt.Println(v) // "3" fmt.Printf("%v\n", v) // "3" fmt.Println(v.String()) // "<int Value>" v1 := reflect.ValueOf("字符串") fmt.Println(v1) //字符串 fmt.Printf("%v\n", v1) //字符串 fmt.Println(v1.String()) //字符串
-
调用 Value 的 Type 方法将返回具体类型所对应的 reflect.Type
t := v.Type() // a reflect.Type fmt.Println(t.String()) // "int"
-
逆操作是调用 reflect.ValueOf 对应的 reflect.Value.Interface 方法. 它返回一个 interface{} 类型表示 reflect.Value 对应类型的具体值
v := reflect.ValueOf(3) // a reflect.Value x := v.Interface() // an interface{} i := x.(int) // an int fmt.Printf("%d\n", i) // "3"
- 一个空的接口隐藏了值对应的表示方式和所有的公开的方法, 因此只有我们知道具体的动态类型才能使用类型断言来访问内部的值
-
返回v持有的值的字符串表示。因为go的String方法的惯例,Value的String方法比较特别。和其他获取v持有值的方法不同:v的Kind是String时,返回该字符串;v的Kind不是String时也不会panic而是返回格式为""的字符串,其中T是v持有值的类型
-
-
reflect.Value 的 Kind 方法来替代之前的类型 switch. 虽然还是有无穷多的类型, 但是它们的kinds类型却是有限的: Bool, String 和 所有数字类型的基础类型; Array 和 Struct 对应的聚合类型; Chan, Func, Ptr, Slice, 和 Map 对应的引用类似; 接口类型; 还有表示空值的无效类型. (空的 reflect.Value 对应 Invalid 无效类型.)
-
-
通过reflect.Value修改值
-
case
x := 2 // value type variable? a := reflect.ValueOf(2) // 2 int no b := reflect.ValueOf(x) // 2 int no c := reflect.ValueOf(&x) // &x *int no d := c.Elem() // 2 int yes (x)
-
其中a对应的变量不可取地址。因为a中的值仅仅是整数2的拷贝副本。b中的值也同样不可取地址。c中的值还是不可取地址,它只是一个指针&x的拷贝。实际上,所有通过reflect.ValueOf(x)返回的reflect.Value都是不可取地址的。但是对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value
-
可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址
fmt.Println(a.CanAddr()) //"false" fmt.Println(b.CanAddr()) //"false" fmt.Println(c.CanAddr()) //"false" fmt.Println(d.CanAddr()) //"true"
-
-
从变量对应的可取地址的reflect.Value来访问变量
-
第一步是调用Addr()方法,它返回一个Value,里面保存了指向变量的指针
-
第二步是在Value上调用Interface()方法,也就是返回一个interface{},里面通用包含指向变量的指针
-
第三步如果我们知道变量的类型,我们可以使用类型的断言机制将得到的interface{}类型的接口强制环为普通的类型指针。这样我们就可以通过这个普通指针来更新变量了
x := 2 d := reflect.ValueOf(&x).Elem() // d refers to the variable x px := d.Addr().Interface().(*int) // px := &x *px = 3 // x = 3 fmt.Println(x) // 3 //也可以不使用指针,而是通过调用可取地址的reflect.Value的reflect.Value.Set方法更新值 d.Set(reflect.ValueOf(4)) fmt.Println(x) // 4 d.SetInt(5) fmt.Println(x) // 5
-
Set方法将在运行时执行和编译时类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
-
通用对一个不可取地址的reflect.Value调用Set方法也会导致panic异常
x := 2 b := reflect.ValueOf(x) b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
-
-
-
reflect.TypeOf和reflect.ValueOf来获取任意对象的Type和Value
type myInt int type Person struct { Name string Age int } //反射获取任意变量的类型 func reflectFn(x interface{}) { v := reflect.TypeOf(x) fmt.Printf("类型:%v 类型名称:%v 类型种类:%v \n", v, v.Name(), v.Kind()) } func main() { a := true b := "golang" reflectFn(a) reflectFn(b) var c myInt = 34 var d = Person{ Name: "张三", Age: 12, } reflectFn(c) reflectFn(d) var e = 10 reflectFn(&e) }
-
反射中关于类型划分为两种:类型(Name())和种类(Kind():底层类型)
-
通过反射设置变量的值
func reflectSetValue(x interface{}) { v := reflect.ValueOf(x) if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(123) } } func main() { var a int64 = 100 reflectSetValue(&a) fmt.Println(a) }
-
结构体反射
type Student struct { Name string `json:"name"` Age int `json:"age"` Score int `json:"score"` } func (s Student) GetInfo() string { var str = fmt.Sprintf("姓名:%v 年龄:%v 成绩:%v", s.Name, s.Age, s.Score) return str } func (s *Student) SetInfo(name string, age int, score int) { s.Name = name s.Age = age s.Score = score } func (s Student) Print() { fmt.Println("打印方法") } //打印字段 func PrintStructField(s interface{}) { t := reflect.TypeOf(s) v := reflect.ValueOf(s) if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct { fmt.Println("不是结构体") return } //通过Field获取结构体字段 field0 := t.Field(0) fmt.Printf("%#v\n", field0) fmt.Println("字段名称:", field0.Name) fmt.Println("字段类型:", field0.Type) fmt.Println("字段Tag:", field0.Tag.Get("json")) //通过FieldByName获取结构体字段 field1, ok := t.FieldByName("Age") if ok { fmt.Println("字段名称:", field1.Name) fmt.Println("字段类型:", field1.Type) fmt.Println("字段Tag:", field1.Tag.Get("json")) } //通过NumField获取结构体有几个字段 var fieldCount = t.NumField() fmt.Println(fieldCount) //通过值变量获取结构体属性对应的值 fmt.Println(v.FieldByName("Name")) } //打印执行方法 func PrintStructFn(s interface{}) { t := reflect.TypeOf(s) v := reflect.ValueOf(s) if t.Kind() != reflect.Struct && t.Elem().Kind() != reflect.Struct { fmt.Println("不是结构体") return } //获取结构体方法 method0 := t.Method(0) //和结构体方法的ASCII有关 fmt.Println(method0.Name) fmt.Println(method0.Type) //获取结构体方法 method1, ok := t.MethodByName("Print") if ok { fmt.Println(method1.Name) fmt.Println(method1.Type) } //通过值变量执行方法 v.Method(1).Call(nil) v.MethodByName("Print").Call(nil) //执行方法传入参数 var params []reflect.Value params = append(params, reflect.ValueOf("李四")) params = append(params, reflect.ValueOf(23)) params = append(params, reflect.ValueOf(99)) v.MethodByName("SetInfo").Call(params) info := v.MethodByName("GetInfo").Call(nil) fmt.Println(info) //获取方法数量 fmt.Println(t.NumMethod()) } //反射修改结构体属性 func reflectChangeStruct(s interface{}) { t := reflect.TypeOf(s) v := reflect.ValueOf(s) if t.Kind() != reflect.Ptr { fmt.Println("不是结构体指针类型") return } else if t.Elem().Kind() != reflect.Struct { fmt.Println("不是结构体指针类型") return } //修改结构体属性的值 age := v.Elem().FieldByName("Age") age.SetInt(24) }
测试
-
palindrome.go
package word func IsPalindrome(s string) bool { for i := range s { if s[i] != s[len(s)-1-i] { return false } } return true }
-
word_test.go
package word import "testing" func TestPalindrome(t *testing.T) { if !IsPalindrome("detartrated") { t.Error(`IsPalindrome("detartrated") = false`) } if !IsPalindrome("kayak") { t.Error(`IsPalindrome("kayak") = false`) } } func TestNonPalindrome(t *testing.T) { if IsPalindrome("palindrome") { t.Error(`IsPalindrome("palindrome") = true`) } }
上一篇: 《Go语言圣经》笔记
下一篇: Hadoop的安装与环境搭建教程图解