Go语言的一些基本特性及要注意的坑
函数
小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
make-new
- make:
make也是用于内存分配的,但是和new不同,它只用于chan、map以及切片的内存创建, 而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。 注意,因为这三种类型是引用类型,所以必须得初始化,但是不是置为零值,这个和new是不一样的。
func make(t Type, size ...IntegerType) Type
- new:
它只接受一个参数,这个参数是一个类型,分配好内存后,返回一个指向该类型内存地址的指针。 同时请注意它同时把分配的内存置为零,也就是类型的零值。
// The new built-in function allocates memory. The first argument is a type,
// not a value, and the value returned is a pointer to a newly
// allocated zero value of that type.
func new(Type) *Type
二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值; 而new用于类型的内存分配,并且内存置为零。 所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。
make返回的还是这三个引用类型本身;而new返回的是指向类型的指针。
byte, rune, string
string存储unicode的话,如果有中文,按下标是访问不到的,因为你只能得到一个byte
总结: rune 能操作 任何字符 byte 不支持中文的操作
值类型/引用类型
值类型分别有:int系列、float系列、bool、string、数组和结构体
引用类型有:指针、slice切片、管道channel、接口interface、map、函数等
值类型的特点是:变量直接存储值,内存通常在栈中分配
引用类型的特点是:变量存储的是一个地址,这个地址对应的空间里才是真正存储的值,内存通常在堆中分配
select会随机选择一个可用通道做收发操作
Go语言局部变量分配, 栈还是堆
Go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。 一个函数内局部变量,不管是不是动态new出来的,它会被分配在堆还是栈,是由编译器做逃逸分析之后做出的决定。
golang 的 runtime 机制
Runtime 负责管理任务调度,垃圾收集及运行环境。同时,Go提供了一些高级的功能,如goroutine, channel, 以及Garbage collection。这些高级功能需要一个runtime的支持. runtime和用户编译后的代码被linker静态链接起来,形成一个可执行文件。这个文件从操作系统角度来说是一个user space的独立的可执行文件。 从运行的角度来说,这个文件由2部分组成,一部分是用户的代码,另一部分就是runtime。runtime通过接口函数调用来管理goroutine, channel及其他一些高级的功能。从用户代码发起的调用操作系统API的调用都会被runtime拦截并处理。
Go runtime的一个重要的组成部分是goroutine scheduler。他负责追踪,调度每个goroutine运行,实际上是从应用程序的process所属的thread pool中分配一个thread来执行这个goroutine。因此,和java虚拟机中的Java thread和OS thread映射概念类似,每个goroutine只有分配到一个OS thread才能运行。
如何获取 go 程序运行时的协程数量, gc 时间, 对象数, 堆栈信息
调用接口 runtime.ReadMemStats 可以获取以上所有信息, 注意: 调用此接口会触发 STW(Stop The World)
参考: https://golang.org/pkg/runtime/#ReadMemStats
如果需要打入到日志系统, 可以使用 go 封装好的包, 输出 json 格式. 参考:
https://golang.org/pkg/expvar/ http://blog.studygolang.com/2017/06/expvar-in-action/ 更深入的用法就是将得到的运行时数据导入到 ES 内部, 然后使用 Kibana 做 golang 的运行时监控, 可以实时获取到运行的信息(堆栈, 对象数, gc 时间, goroutine, 总内存使用等等), 具体信息可以看 ReadMemStats 的那个结构体
调试 golang 的 bug 以及性能问题
panic调用栈
pprof
火焰图(配合压测)
使用go run -race 或者 go build -race 来进行竞争检测
查看系统 磁盘IO/网络IO/内存占用/CPU 占用(配合压测)
Golang通过哪几种方式来实现并发控制,如何优雅的退出goroutine?
1.chan 通过无缓冲通道来实现多 goroutine 并发控制
2.通过sync包中的WaitGroup 实现并发控制
退出:
使用for-range退出
使用,ok退出
使用退出通道退出
Golang的interface的特性和技巧
1.空接口(empty interface)
空接口比较特殊,他不包含任何方法,但是他又可以表示任何类型
golang的所有基础类都实现了空接口,所有我们可以用[]interface表示结构不同的数组
2.接口嵌套接口
3.类型的选择与断言
用通道实现主程等待协程2s,如果超过2s,主程直接结束(不用sleep)
func main() {
start := time.Now()
wait := make(chan int,1)
go func() {
fmt.Println("do sth")
time.Sleep(1*time.Second)
wait<-2
}()
fmt.Println("main process")
select {
case nums:= <-wait:
fmt.Println(nums)
case <-time.After(2*time.Second):
fmt.Println("2 sec ...")
}
fmt.Println(time.Since(start))
}
继承多态
“组合优于继承”
const
Go语言预定义了这些常量:true、false和iota; iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1; 枚举指一系列相关的常量,比如下面关于一个星期中每天的定义; 可以用在const后跟一对圆括号的方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值; Go语言并不支持众多其他语言明确支持的enum关键字; 下面是一个常规的枚举表示法,其中定义了一系列整型常量:
const (Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays
// 这个常量没有导出
)
浮点数比较
因为浮点数不是一种精确的表达方式,所以像整型那样直接用==来判断两个浮点数是否相等是不可行的, 这可能会导致不稳定的结果。 下面是一种推荐的替代方案:
import "math"
const p = 0.0000001
func IsEqual(f1, f2 float64) bool {
return math.Abs(f1-f2) < p
}
字符
每个中文字符在UTF-8中占3个字节,而不是1个字节 以Unicode字符方式遍历时,每个字符的类型是rune
内置函数delete
Go语言提供了一个内置函数delete(),用于删除容器内的元素。
用delete()函数删除map内的元素:
delete(myMap, "1234")
上面的代码将从myMap中删除键为“1234”的键值对。如果“1234”这个键不存在,那么这个调
用将什么都不发生,也不会有什么副作用。
但是如果传入的map变量的值是nil,该调用将导致程序抛出异常(panic)。
switch
-
与C语言等规则相反,Go语言不需要用break来明确退出一个case;
-
只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case;
break
- Go语言的for循环同样支持continue和break来控制循环,但是它提供了一个更高级的 break,可以选择中断哪一个循环,如下例:
func BreakLoop() {
Loop:
for j := 0; j < 3; j++ {
fmt.Println(j)
for a := 0; a < 5; a++ {
fmt.Println(a)
if a > 3 {
break Loop
}
}
}
}
break语句终止的是Loop标签处的外层循环。
跳转语句
goto语句被多数语言学者所反对,谆谆告诫不要使用。
goto还是会在一些场合下被证明是最合适的
goto语句的语义非常简单,就是跳转到本函数内的某个标签,如:
func GotoFunc() {
i := 0
HERE:
fmt.Println(i)
i++
if i < 10 {
goto HERE
}
}
不定参数类型
不定参数是指函数传入的参数个数为不定数量。
为了做到这点,首先需要将函数定义为接受不定参数类型:
func MyFunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
func TestMyFunc(t *testing.T) {
args := []int{1, 2, 3}
MyFunc(args...)
MyFunc(4, 5)
MyFunc(6, 7, 8, 9)
}
闭包
func AnonymFunc() {
j := 5
a := func() {
i := 10
fmt.Printf("i: %d, j: %d\n", i, j)
}
a()
j *= 2
a()
}
struct
Go语言的结构体(struct)和其他语言的类(class)有同等的地位, 但Go语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。 所有的Go语言类型(指针类型除外)都可以有自己的方法。
type Rect struct {
x, y float64
width, height float64
}
然后我们定义成员方法Area()来计算矩形的面积:
func (r *Rect) Area() float64 {
return r.width * r.height
}
godoc
godoc -http=:76 -path=”.” 然后再访问http://localhost:76/,单击顶部的foo链接,或者直接访问http://localhost:76/pkg/foo/, 就可以看到注释提取的效果
注释
若要将注释提取为文档,要遵守如下的基本规则。
-
注释需要紧贴在对应的包声明和函数之前,不能有空行。
-
注释如果要新起一个段落,应该用一个空白注释行隔开,因为直接换行书写会被认为是正常的段内折行。
-
开发者可以直接在代码内用// BUG(author): 的方式记录该代码片段中的遗留问题,这些遗留问题也会被抽取到文档中。
反射
反射(reflection)是在Java出现后迅速流行起来的一种概念。 通过反射,你可以获取丰富的类型信息,并可以利用这些类型信息做非常灵活的工作。
reflect.go
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
对结构的反射操作
type T struct {
A int
B string
}
t := T{203, "mh203"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
Go邮件组
Go邮件组的地址为http://groups.google.com/group/golang-nuts
Go的中文邮件组为http://groups.google.com/group/golang-china
tips
-
简短声明的变量只能在函数内部使用
-
显式类型的变量无法使用 nil 来初始化
nil 是 interface、function、pointer、map、slice 和 channel 类型变量的默认初始值。 但声明时不指定类型,编译器也无法推断出变量的具体类型。
- Array 类型的值作为函数参数
与Python一致,与C/C++不一致
在 Go 中,数组是值。作为参数传进函数时,传递的是数组的原始值拷贝,此时在函数内部是无法更新该数组的:
func ChangeArrItem(arr [3]int64) {
arr[0] = 100
}
func ChangeSliceItem(arr []int64) {
arr[0] = 100
}
func TestChangeArrItem(t *testing.T) {
arr := [3]int64{1, 2, 3}
ChangeArrItem(arr)
fmt.Println(arr)
s := []int64{1, 2, 3}
ChangeSliceItem(s)
fmt.Println(s)
}
=== RUN TestChangeArrItem
[1 2 3]
[100 2 3]
- slice
当你从一个已存在的 slice 创建新 slice 时,二者的数据指向相同的底层数组。如果你的程序使用这个特性,那需要注意 “旧”(stale) slice 问题。
某些情况下,向一个 slice 中追加元素而它指向的底层数组容量不足时,将会重新分配一个新数组来存储数据。而其他 slice 还指向原来的旧底层数组。
- slice 和 array 其实是一维数据
https://stackoverflow.com/questions/39561140/go-how-is-two-dimensional-arrays-memory-representation
https://stackoverflow.com/questions/39804861/what-is-a-concise-way-to-create-a-2d-slice-in-go
- string 类型的值是常量,不可更改
func StringIndex() {
s := "abcdefg"
// s[1] = "1"
sBytes := []byte(s)
sBytes[0] = []byte("Q")[0]
fmt.Println(sBytes, string(sBytes))
sRunes := []rune(s)
sRunes[0] = []rune("在")[0]
fmt.Println(sRunes, string(sRunes))
// === RUN TestStringIndex
// [81 98 99 100 101 102 103] Qbcdefg
// [22312 98 99 100 101 102 103] 在bcdefg
}
func TestStringIndex(t *testing.T) {
StringIndex()
}
- string 与索引操作符
s := "abcdef"
fmt.Println(s[0])
- 字符串并不都是 UTF8 文本
utf8.ValidString()
-
Go 的内建函数 len() 返回的是字符串的 byte 数量,而不是像 Python 中那样是计算 Unicode 字符数。
-
slice的初始化,{}
x := []int {
1,
2 // syntax error: unexpected newline, expecting comma or }
}
y := []int{1,2,}
z := []int{1,2}
// ...
-
switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。
-
Go 重用 ^ XOR 操作符来按位取反
-
程序默认不等所有 goroutine 都执行完
-
向已关闭的 channel 发送数据会造成 panic
-
在一个值为 nil 的 channel 上发送和接收数据将永久阻塞
-
defer函数的参数值,对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值
var i = 1
defer fmt.Println("result: ", func() int { return i * 3 }())
i++
// result: 3
- 关闭 HTTP 的响应体
defer resp.Body.Close()
// resp 可能为空
- 关闭 HTTP 连接
一些支持 HTTP1.1 或 HTTP1.0 配置了 connection: keep-alive 选项的服务器会保持一段时间的长连接。但标准库 “net/http” 的连接默认只在服务器主动要求关闭时才断开,所以你的程序可能会消耗完 socket 描述符。
req.Close = true
req.Header.Add("Connection", "close")
http.Transport{DisableKeepAlives: true}
- encode/decode JSON 数据时,Go 默认会将数值当做 float64 处理
syncPost["project_id"].(float64)
- 从一个现有的非 interface 类型创建新类型时,并不会继承原有的方法
使用组合
- 跳出 for-switch 和 for-select 代码块
func BreakLoop() {
Loop:
for j := 0; j < 3; j++ {
fmt.Println(j)
for a := 0; a < 5; a++ {
fmt.Println(a)
if a > 3 {
break Loop
}
}
}
}
- defer
对 defer 延迟执行的函数,它的参数会在声明时候就会求出具体值,而不是在执行时才求值
func VarInDefer() {
var i = 1
defer fmt.Println("result: ", func() int { return i * 2 }())
i++
}
// 2
defer在函数返回时才执行,而不是在调用语句块结束时执行
defer延迟执行的函数写入匿名函数中
for k := range files {
f, _ := os.Open(files[k])
defer f.Close()
}
for k := range files {
func(){
f, _ := os.Open(files[k])
defer f.Close()
}()
}
refs
https://segmentfault.com/a/1190000013739000