Go语言的一些基本特性及要注意的坑

发布于 2017-06-14 · 本文总共 7698 字 · 阅读大约需要 22 分钟

函数

小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。

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




本博客所有文章采用的授权方式为 自由转载-非商用-非衍生-保持署名 ,转载请务必注明出处,谢谢。
声明:
本博客欢迎转发,但请保留原作者信息!
博客地址:邱文奇(qiuwenqi)的博客;
内容系本人学习、研究和总结,如有雷同,实属荣幸!
阅读次数:

文章评论

comments powered by Disqus


章节列表