这篇Blog主要记录平时学习和使用Go语言所遇到的不清楚和容易搞混淆的知识点。
Tips:以下代码在go1.12.6 windows/amd64版本下测试分析,版本不同在分析源码的时候略有不同。
目录
-
Go内建函数new和make
func make(t Type, size ...IntegerType) Type
make只用于slice,map,channel,并且返回这些类型的引用。
func new(Type) *Type
new用于一切类型,并且返回指向初始化类型内存的指针。
package main
import "fmt"
type T struct {
a int
b string
}
func main(){
a := new([]int)
b := make([]int,0)
fmt.Printf("%T %T\n",a,b)
c := new(int)
fmt.Printf("%T\n",c)
*c = 1
fmt.Printf("%v\n",*c)
//var d *int
//*d = 2 //panic
//fmt.Printf("%v\n",*d)
e := new(T)
e.a = 3
e.b = "hello"
fmt.Printf("%v\n",e)
fmt.Printf("%v %v\n",e.a,e.b)
}
具体可以参考Go官方文档解释:https://golang.org/pkg/builtin/#make
-
runtime.Caller详解
最近在对公司项目服务器日志系统进行优化,本来日志打印就是使用log.New生成的Loger直接使用,对于Logger的out是文件。但是有个问题是,如果服务器日志量很大,产生的日志文件很大的话文件就无法打开。所以针对这个,决定每天(或者几个小时)生成一个新的out文件,所以不能直接使用自带的log,需要封装一层,类似:
type LoggerObject struct {
mutex sync.Mutex
l *log.Logger
f *os.File
t time.Time
}
如果要写入日志,之前直接调用Logger的Print/Printf/Println等方法。现在我们为LoggerObject添加相应的Print/Printf/Println等方法给使用者直接调用。第一次看到log包的这些函数,比如:
当时就很好奇这个参数2是什么意思,但是也没在意。然后我也模仿这个写:
func (l *LoggerObject)Printf(format string, v ...interface{}) {
//这里可以做一些别的事情
//....
l.l.Printf(format,v...)
}
(Tips:对于省略号"..."不清楚的,转到Go 省略号"..."使用总结)
但是你打印的出来的日志前缀信息中文件名,行数等一直都是上面l.l.Printf(format,v...)所在的文件名和行数。所以开始看源码,发现确实和这个参数2有关系。看源码:
通过一个runtime.Caller函数得到第calldepth层函数的文件名,所在行数等信息,然后将这个写到每个日志前缀中。可以看看Caller这个源码,如下:
从解释来看:参数skip是往上第skip层调用Caller的堆栈层数,0标识Caller的调用方,并且返回skip层Caller调用方的file和line信息。看下面例子:
package main
import (
"fmt"
"os"
"runtime"
"strconv"
)
func f11(depth string) (f string ,l int){ //calldepth = 0
d,_ := strconv.Atoi(depth)
_, file, line, ok := runtime.Caller(d)
if !ok {
file = "???"
line = 0
}
return file,line
}
func f22(depth string) (f string ,l int) { //calldepth = 1
return f11(depth)
}
func main() { //calldepth = 2
f,l := f22(os.Args[1])
fmt.Printf("%v %v\n",f,l)
}
从结果来看一目了然了,将f11和f22放到别的包中,文件名字也会不同。另外从第四个结果可以看出,可以看到main函数是从哪里开始调用的。
再看Caller源码解释的时候,看到还有一个函数Callers和Caller挺相似的。如下:
但是好像并没有返回什么调用者信息,但是它实际返回了,但是需要进行转化以下,通过Caller启发得到如下例子:
package main
import (
"fmt"
"os"
"runtime"
"strconv"
)
func f12(depth string) {
d,_ := strconv.Atoi(depth)
rpc := make([]uintptr, 1)
runtime.Callers(d+1,rpc) // 需要判断返回值,这里简化了
frame, _ := runtime.CallersFrames(rpc).Next()
fmt.Printf("%v %v %v\n",frame.File,frame.Function,frame.Line)
}
func main() {
f12(os.Args[1])
}
可以发现通过Callers+runtime.CallersFrames(rpc).Next()得到Frame实例(即frame),通过frame能得到更具体的调用者信息,不局限于Caller的file和line信息。
还有一点需要注意:runtime.Callers(d+1,rpc),这里d+1很重要,因为在Callers还要进入callers函数,不然的话,0是在调用callers函数开始不是Callers函数。
-
Contains方法
// 判断obj是否在target中,target支持的类型arrary,slice,map
func Contain(obj interface{}, target interface{}) (bool, error) {
targetValue := reflect.ValueOf(target)
switch reflect.TypeOf(target).Kind() {
case reflect.Slice, reflect.Array:
for i := 0; i < targetValue.Len(); i++ {
if targetValue.Index(i).Interface() == obj {
return true, nil
}
}
case reflect.Map:
if targetValue.MapIndex(reflect.ValueOf(obj)).IsValid() {
return true, nil
}
}
return false, errors.New("not in array")
}
参考原文:https://studygolang.com/articles/271
-
换行与分号
先看一个例子:
func test() {
x1 := []int{
1,2,3,
4,5,6,
}
x2 := []int{
1,2,3,
4,5,6,}
//x3 := []int{ //error
// 1,2,3,
// 4,5,6
//}
x4 := []int{
1,2,3,
4,5,6}
x5 := []int{1,2,3,4,5,6,}
fmt.Print(x1,x2,x4,x5)
}
Go语言编译器会自动在以标识符、数字字面量、字母字面量、字符串字面量、特定的关键字(break、continue,fallthrough和return)、增减操作符(++和--)、或者一个右括号和右方括号和右大括号(即)、]、})结束的非空行的末尾自动加上分号。
注意,i++
和i--
在Go
语言中是语句,不是表达式,因此不能赋值给另外的变量。此外没有++i
和--i
。
参考原文:https://blog.csdn.net/stpeace/article/details/81697347 (偶然发现CSDN排名第一的大佬)
-
nil channel
channel默认值是nil,即未初始化。那nil channel有什么作用呢?
- <-c 从 c 接收将永远阻塞
- c <- v 发送值到 c 会永远阻塞
- close(c) 关闭 c 引发 panic
对nil channel更详细用法见:https://lingchao.xin/post/why-are-there-nil-channels-in-go.html
-
Go哪些数据类型可以赋值nil
当然还有interface变量也可以赋值为nil。所以有以下类型可以赋值为nil:
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil
-
Go交叉编译
在Windows编译linux系统执行的程序
windows环境
linux版本
在windows终端修改GOARCH和GOOS环境变量即可:
注意不需要修改CGO_ENABLED=0,不然会出现:
记住,编译好了之后把环境变量修改回去。
-
Go逃逸分析
参考地址:
https://my.oschina.net/renhc/blog/2222104
https://www.cnblogs.com/wilburxu/p/11184604.html
在阅读源码的时候经常看到注释指令:go:noescape
该指令指定下一个有声明但没有主体(意味着实现有可能不是 Go)的函数,不允许编译器对其做逃逸分析。一般情况下,该指令用于内存分配优化。因为编译器默认会进行逃逸分析,会通过规则判定一个变量是分配到堆上还是栈上。但凡事有意外,一些函数虽然逃逸分析其是存放到堆上。但是对于我们来说,它是特别的。我们就可以使用 go:noescape 指令强制要求
编译器将其分配到函数栈上。
另外还有一些注释指令请参考:https://cloud.tencent.com/developer/article/1422358
在上面说到看到,使用命令来查看变量是否逃逸到堆时,需要添加'-l'选项禁止编译器优化进行将函数内联。那么Go语言编译内联是什么?
其实和c++作用是一样的。例如a(){ b(){ xxx } },则编译器直接将b()代码展开到a函数里面,等同a(){ xxx },减少函数调用的开销。前提函数b是小函数且内部不再调用其它函数。
-
Go JSON编解码
详情请看作者blog:Go JSON编解码
-
接口
详情请看作者blog:通过汇编和源码两大神器探究 —— Go语言接口
-
slice
详情请看作者blog:通过汇编和源码两大神器探究 —— Go语言Slice
-
Go语言汇编
详情请看作者blog:Go语言汇编入门
-
int int32 int64
64位机器 | 32位机器 | |
int | 8字节 | 4字节 |
int32 | 4字节 | 4字节 |
int64 | 8字节 | 8字节 |
-
^和&^
^ 异或运算,即不进位加法计算。例如:0000 0100 + 0000 0010 = 0000 0110 = 6
&^ AND NOT。例如:0000 0010(x) &^ 0000 0100(y) = 0000 0010 如果y bit位上的数是0则取x上对应位置的值,如果y bit位上为1则取结果位上取0。
-
Go语言指针
Go语言虽然可以取变量指针,但是不能对变量的指针直接进行数学运算。需要通过间接手段对其地址进行数学运算。如下图所示,先取变量地址,然后转成unsafe.Pointer,再转成uintptr类型,进行数学运算。
- 任何类型的指针可以转成unsafe.Pointer。
- unsafe.Pointer可以转成任何类型的指针。
- unsafe.Pointer的值(即也是传进来变量的地址)不能进行数学运算,比如偏移。必须要转成uintptr类型进行数学运算。
实例:
func main() {
a := 1000
s := "hello world"
//任何类型的指针可以转成unsafe.Pointer。
pa1 := unsafe.Pointer(&a)
ps1 := unsafe.Pointer(&s)
fmt.Println(&a) //0xc042062080
fmt.Println(&s) //0xc0420561c0
fmt.Println(pa1) //0xc042062080
fmt.Println(ps1) //0xc0420561c0
//unsafe.Pointer可以转成任何类型的指针。
pa2 := (*int32)(unsafe.Pointer(&a)) //int32占4字节
fmt.Println(*pa2) //1000 1111101000
pa3 := (*byte)(unsafe.Pointer(&a)) //byte占1字节
fmt.Println(*pa3) //232 11101000 截取第1字节的数据
//指针地址不能直接进行数学运算,要转成uintptr类型进行数学运算
arr := [3]int{1,2,3}
ps2 := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0]))+unsafe.Sizeof(int(0))))
fmt.Println(*ps2) //2
}
-
Go语言之TCP
详情请看作者blog:使用Go和C实例来探究Linux TCP之listen backlog
-
Go runtime调度图
-
Go内存分配
-
未完待续