Go圣经
- i++是语句,而不是表达式,所以类似
j=i++
非法,并且++只能在后边 - 常量目前只能是数值、字符串或者一个固定的布尔值
变量
基本类型:数值、字符串、布尔值
引用类型:指针、接口、切片、map、函数、chan
聚合类型:数组、结构体
- Go中声明变量不初始化的情况下都会默认零值,不会出现undefined的情况
引用类型的零值为nil
基本类型为其对应的零值
聚合类型的元素或字段为其对应的零值
- 短变量声明对已经声明过的变量只有赋值作用,但是多次变量声明必须要有一个是新的变量
res,err:=test()
res1,err:=test()//ok
res:=test()
res:=test()//error
- 不是每个值都有内存地址,但是每个变量一定有内存地址,换句话说变量是可寻址的(也不是一定,
reflect.Value
类型变量事不可取地址的) -
p:=new(T)
声明一个T类型的零值变量,返回其地址,每次返回都是新的变量地址,但是可能会相同,如果两个类型为空时,即类型的大小为0,如struct{}
和[0]int
,返回地址可能是同一个,并且当大小为0时,垃圾回收器有不同行为,应该避免这种用法。
整形
- Go中取模结果符号与被取模运算符的符号是一致的(同Javascript),除法结果依赖于操作数是否为整数,只要有一个为浮点数那么就是正常除法
- 打印数据时
%[1]
为继续取第一个操作符,如fmt.Println("%d %[1]c %[1]q\n",ascii)
字符串
字符串的第ige索引值代表的是第i个字节,第i个字节不一定是第i个字符,因为UTF-8是一种变长的编码方式,对于非ASCII编码可能会使用两个或者更多字节进行编码,可以使用[]rune(string)
将字符串转化为unicode码点后进行索引,一个unicode码点为4个字节
func main() {
s := "国hello, world"
fmt.Println(string(s[0]), string([]rune(s)[0])) // å 国
}
常量
golang也可以进行类似枚举的操作(只能是常量),使用iota
,从0开始递增
var (
A int = 1 << iota
B
C
D
)
func main() {
// var a Vex
fmt.Println(A, B, C)
}
golang许多常量可能并没有一个明确的基础类型,在赋值时会进行尝试转换
import "fmt"
func main() {
var a float64 = 3 + 0i
fmt.Printf("类型:%T,值:%[1]v\n", a) // 类型:float64,值:3
a = 20
fmt.Printf("类型:%T,值:%[1]v\n", a) // 类型:float64,值:20
a = 'a'
fmt.Printf("类型:%T,值:%[1]v\n", a) // 类型:float64,值:97
}
复合数据类型
数组
- 数组可以使用索引的形式进行声明:
[...]int{99:1}
声明长度为100,最后一个元素值为1的数组。 - 数组区别与JavaScript不是引用类型,数组的比较相等判断对应元素是否相等
-
append
像切片添加内容时,如果底层数组容量足够,那么直接是在底层数组上添加,然后返回切片值,如果容量我够则扩容生成一个新的底层数组,所以在使用append向切片添加内容时我们不确定返回的是之前的底层数组的切片值还是一个新的底层数组
func test(arr []int) {
arr = append(arr, 3)
arr[0] = 3
fmt.Println(len(arr), cap(arr)) // 3 4
}
func main() {
arr := make([]int, 2, 4)
test(arr)
fmt.Println(arr[:3]) //[3 0 3] 共享同一底层数组,扩容后可以看见添加的值
fmt.Println(len(arr), cap(arr)) // 2 4
}
所以平常可以这么使用,避免一些不必要的麻烦(覆盖原始数据之类的)
func test(arr []int) []int {
arr = append(arr, 3)
arr[0] = 3
return arr
}
func main() {
arr := make([]int, 2, 4)
arr = test(arr)
fmt.Println(arr) // 2 4
}
map
- map的值是不可取地址的,原因在于map随着元素增加会分配更大的内存空间,之前分配的空间可能失效
函数
可变参数
golang支持可变参数,类型必须匹配
func test(v ...int) {
for _, value := range v {
fmt.Println(value)
}
}
func main() {
test(12, 3, 3)
arr := []int{5, 6, 7}
test(arr...)
}
匿名函数
golang中也要注意循环闭包带来的引用同一变量的问题,类似JavaScript
Recover
golang异常可以使用recover()
恢复,但是不要滥用,在该恢复的地方进行恢复
func main() {
defer func() {
if p := recover(); p != nil {
fmt.Println("调用recover可以恢复异常,注意应该只恢复应该恢复的panic")
}
}()
log.Panic("异常")
log.Println("啊啊啊啊啊")
}
接口
- 空接口类型不能对值进行操作,因为他没有实现任意方法
- 只有当两个或两个以上具体类型需要使用同样的方法处理时才应该使用接口
包和工具
包的重命名
import (
"crypto/rand"
"fmt"
mrand "math/rand"
)
- golang中出现循环依赖会报错
测试
创建test目录,为file包创建file_test.go文件,test文件测试函数必须以Test
开始。调用go test -v
进行测试。demo:
func TestSum(t *testing.T) {
if Sum(2, 3) != 5 {
t.Error("错误:2+3!=5")
}
if Sum(4, 6) == 8 {
t.Error("错误:4+6=8")
}
}
go test ./...
可以进行递归测试
测试覆盖率查看
- go test -v -coverprofile=cover.out
- go tool cover -html=c.out
第二部是转化为html查看
反射
反射可以帮助我们在运行时更新变量或者检查他们的值,调用方法和内在操作等,interface{}
需要在运行时确定类型,但是如果不是确定其动态类型并且调用断言的话,是不能访问其内在的值和方法的,但是反射提供了这种机制。
package main
import (
"fmt"
"reflect"
)
func test(data interface{}) {
t := reflect.TypeOf(data) // 返回reflect.Type
fmt.Printf("%q\n", t) // 打印时会调用String方法
v := reflect.ValueOf(data) // 返回reflect.Value类型
fmt.Printf("%q\n", v)
fmt.Println("---------", v.Kind() == reflect.String) // 判断类型,不能与string比较,因为其是无类型string
t = v.Type() // 调用Type 方法返回对应具体类型的reflect.Type
fmt.Printf("%q\n", t)
// reflect.ValueOf(data)的逆操作,返回interface{}类型的具体值
// 与reflect.Value不同的是空接口隐藏了值的内部表现和方法(什么都
// 做不了),只有知道具体的动态类型时才能使用断言访问内部值。相反
// reflect。Value有很多方法检查其内容
i := v.Interface()
fmt.Printf("%q\n", i.(string))
}
func main() {
test("字符串")
}
通过反射修改值
package main
import (
"fmt"
"reflect"
)
func main() {
x := 3
a := reflect.ValueOf(&x).Elem() // 获取可取地址的Value,reflect.ValueOf()返回的是值的拷贝
fmt.Println(a.CanAddr()) // 可以使用该方法判断是否可取地址
/*
Addr() 返回reflect.Value 保存指向变量的指针
随后取Interface + 断言即可通过普通指针更新变量
*/
// a.Addr().Interface().(*int)
// 或者调用可取地址的reflect.Value的Set方法更新值
a.Set(reflect.ValueOf(4))
// 简便写法
a.SetInt(6)
y := struct{ field int }{10}
// 可以获取未导出字段,但是不能更改
field := reflect.ValueOf(&y).Elem().FieldByName("field")
// 判断是否可更改
fmt.Println(fd.CanAddr(), fd.CanSet()) // "true false"
}
获取结构体字段标记
type resp struct {
Code int `http:"responseNo"`
Msg string `http:"responseMsg"`
}
func main() {
res := resp{10000, "成功"}
v := reflect.ValueOf(&res).Elem()
fmt.Printf("字段有:%d个\n", v.NumField()) // 获取字段数
fmt.Println(v.Type().Field(1).Tag.Get("http")) // responseMsg
}
调用方法
reflect.Type
和 reflect.Value
都实现了reflect.Mehtod
,通过Call
调用方法,传入参数为[]reflect.Value
类型,返回值同样为[]reflect.Value
类型
// 必须要是导出方法才能获取
func (r resp) Fn() int {
return 123
}
func (r resp) fn() int {
return 123
}
func main() {
res := resp{10000, "成功"}
v := reflect.ValueOf(res) // 值类型的reflect.Value 则获取方法的时候获取接受者为值的方法
fmt.Printf("有%d个方法\n", v.NumMethod()) // 有1个方法
// reflect.Type 和reflect.Value 均实现了reflect.Method
t := v.Type()
fmt.Printf("有%d个方法\n", t.NumMethod()) // 有一方犯法
// Call 调用方法,调用方法传入的参数必须为[]reflect.Value 返回也是[]reflect.Value
// 调用Interface 返回interface{},值是具体值
fmt.Println(v.Method(0).Call(nil)[0].Interface().(int))
}
goroutines和channels
- 带缓存的通道与不带缓存的区别在于,带缓存的通道维护一个队列,好比蛋糕店做蛋糕的两个师傅,如果不带缓存其中一个做完必须等待另外一个师傅进行交接,此时是阻塞状态。带缓存通道好比有个专门存放蛋糕的地方,其中一个师傅做完后可以放进存放的地方,继续做下一个工作
基于共享变量的并发
- 当事件x与事件y不知道是事件x先于事件y调用还是后调用时,那么可以说事件x与事件y是并发的
goroutine与线程的区别
- 具有动态栈
线程运行时会分配一个大约2MB左右(一般语言中)的内存栈用于存储线程运行期间,函数内部的变量等,但是如果每个goroutine都分配这么大的内存的话有些浪费,goroutine一般分配内存大小为2kb左右,并且可以动态改变,最大可达1GB - 调度开销小
线程的调度会通过内核函数完成,golang具有自己调度器,使用内部实现的结构进行调度 - 没有特定标识(屏蔽了)
避免滥用