Go语言学习笔记(持续更新)

这篇Blog主要记录平时学习和使用Go语言所遇到的不清楚和容易搞混淆的知识点。

Tips:以下代码在go1.12.6 windows/amd64版本下测试分析,版本不同在分析源码的时候略有不同。

目录

Go内建函数new和make

runtime.Caller详解

Contains方法

换行与分号

nil channel

Go哪些数据类型可以赋值nil

Go交叉编译

Go逃逸分析

Go JSON编解码

接口​

slice

Go语言汇编

int int32 int64

^和&^

Go语言指针

Go语言之TCP

Go runtime调度图

Go内存分配

未完待续


  • 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位机器
int8字节4字节
int324字节4字节
int648字节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类型,进行数学运算。

  1. 任何类型的指针可以转成unsafe.Pointer。
  2. unsafe.Pointer可以转成任何类型的指针。
  3. 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内存分配

  • 未完待续

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值