go语言学习小结
最近新的工作一些代码是go语言写的,学了一些go语言的基本语法。后期有一些别的学习总结,在此文档更新。
1语法
1.1变量/常量定义
定义在函数外面的变量如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用.
var v Type
var v Type = value
var v = value
var v1,v2,v3 Type
var v1,v2,v3 Type = value1,value2,value3
var v1,v2,v3= value1,value2,value3
v1,v2,v3:= value1,value2,value3
i,j = j,i
:=为简短声明,编译器根据初始化的值自动推断相应的类型,但是不能定义在函数外部使用。
const(
s = “string”
pi = 3.1415
pi2
)
var(
i int
pi float32 = 3.1415
)
1.2 array & slice
array定义
var a [2]int = [2]int{1,2}
a := [2]int{1,2}
a := [...]int{1,2,3,4}
a2 := [2][3]int{[3]int{1,2,3},[3]int{3,2,1}}
a2 := [2][3]int{{1,2,3},{3,2,1}}
数组作为参数时的值传递
package main
import "fmt"
func modify(array [5]int) {
array[0] = 10
fmt.Println("In modify(), array values:", array)
}
func main() {
array := [5]int{1,2,3,4,5}
modify(array)
fmt.Println("In main(), array values:", array)
}
结果:
In modify(), array values: [10 2 3 4 5]
In main(),
array values: [1 2 3 4 5]
slice定义
slice是引用类型
var s []int = make([]int,n)
s := []int{v1,v2,v3}
slice是引用,举例:!!!!!!!!!!
array := []int{10, 11, 12, 13, 14}
slice := array[0:4] // slice是对array的引用
slice[0] = 20
slice[1] = 21
fmt.Println("array: ", array) // array: [20 21 12 13 14]
fmt.Println("slice: cap=", cap(slice), ", value=", slice) // slice: cap= 5 , value= [10 11 12 13]
slice append 使用说明:
例子:
nums:=[]int{1,2,3}
result:=append(nums,4)
fmt.Println(result)
fmt.Println(nums)
输出result的值为:[1 2 3 4] [1 2 3]
首先需要了解append函数实现原理:
1. 如果nums的cap够用,则会直接在nums指向的数组后面追加元素,返回的slice和原来的slice是同一个对象。显然,这种情况下原来的slice的值会发生变化!
2. 如果nums的cap不够用(上述代码就是这种情况),则会重新分配一个数组空间用来存储数据,并且返回指向新数组的slice。这时候原来的slice指向的数组并没有发生任何变化!
1.3 map
var m map[keyType]valueType //keyType可以是string及完全定义了==/!=操作的类型
m := make(map[keyType]valueType)
m:=map[keyType]valueType{k1:v1,k2:v2,k3:v3}
len(m map[keyType]valueType)int
delete(m map[Type]Type1,key Type)
支持“==”判断的类型都可以作为map 的key。不支持“==”的类型为slic 、map、func
1.4 if语句
a)条件语句不需要使用括号将条件括起来;
b)无论语句体有几条语句,花括号{}都是必须存在的;
c)左花括号{ 必须与if或者else在同一行;
d)在if之后,条件语句之前,可以添加变量初始化语句,用;间隔;
e)在有返回值的函数中,不允许将“最终的”return语句包含在if...else...中,否则会编译错误。
1.5 go语句
go fmt.Println("Go!") go语句仅由一个关键字go和一条表达式语句构成。
go语句的执行与其携带的表达式语句的执行在时间上没有必然联系。这里能够确定的仅仅是后者会在前者完成之后发生。在go语句被执行时,其携带的函数(也被称为go函数)以及要传给它的若干参数(如果有的话)会被封装成一个实体(即Goroutine),并被放入到相应的待运行队列中。Go语言的运行时系统会适时的从队列中取出待运行的Goroutine并执行相应的函数调用操作。注意,对传递给这里的函数的那些参数的求值会在go语句被执行时进行。这一点也是与defer语句类似的。
一句话就是go 语句就是起一个线程。
1.6 select 语句
select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。select随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
以下描述了 select 语句的语法:
(1)每个case都必须是一个通信
(2)所有channel表达式都会被求值
(3)所有被发送的表达式都会被求值
(4)如果任意某个通信可以进行,它就执行;其他被忽略。
(5)如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
否则:
如果有default子句,则执行该语句。
如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel 或值进行求值。
1.7 nil解释
(1)nil并不是Go的关键字之一,nil的意思是无,或者是零值。在Go语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。这是每种类型对应的零值:
bool -> false
numbers -> 0
string -> ""
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil
(2)对于指针对象的方法来说,就算指针的值为nil
也是可以调用的;
(3)一个为nil的slice,除了不能索引外,其他的操作都是可以的,当你需要填充值的时候可以使用append函数,slice会自动进行扩充。
// nil slices
var s []slice
len(s) // 0
cap(s) // 0
for range s // iterates zero times
s[i] // panic: index out of range
(4)对于nil的map,我们可以简单把它看成是一个只读的map,不能进行写操作,否则就会panic。
// nil maps
var m map[t]u
len(m) // 0
for range m // iterates zero times
v, ok := m[i] // zero(u), false
m[i] = x // panic: assignment to entry in nil map
(5)关闭一个nil的channel会导致程序panic。+
(6)interface并不是一个指针,它的底层实现由两部分组成,一个是类型,一个值,也就是类似于:(Type, Value)。只有当类型和值都是nil的时候,才等于nil。
golang的基本类型不能赋值nil:
bool
int
uint
float
complex
byte
rune
string
struct
golang的非基本类型都能赋值nil:
array
slice
map
chan
func
interface
golang的指针类型也能赋值nil:
pointer
1.8 管道(Channel)
管道是Go语言在语言级别上提供的goroutine间的通讯方式,我们可以使用channel在多个goroutine之间传递消息。channel是进程内的通讯方式,是不支持跨进程通信的,如果需要进程间通讯的话,可以使用Socket等网络方式。
管道是类型相关的,即一个管道只能传递一种类型的值。管道中的数据是先进先出的。
语法:
1 // 声明方式,在此ElemType是指此管道所传递的类型
2 var chanName chan ElemType
3 // 声明一个传递类型为int的管道
4 var ch chan int
5 // 声明一个map,元素是bool型的channel
6 var m map[string] chan bool
7
8 // 定义语法,定义需要使用内置函数make()即可,下面这行代码是声明+定义一个整型管道
9 ch := make(chan int)
10 // 事先定义好管道的size,下面这行代码定义管道的size为100
11 ch := make(chan int, 100)
12
13 // 由管道中读写数据,<-操作符是与最左边的chan优先结合的
14 // 向管道中写入一个数据,在此需要注意:向管道中写入数据通常会导致程序阻塞,直到有
15 // 其他goroutine从这个管道中读取数据
16 ch<- value
17 // 读取数据,注意:如果管道中没有数据,那么从管道中读取数据会导致程序阻塞,直到有数据
18 value := <-ch
19
20 // 单向管道
21 var ch1 chan<- float64 // 只能向里面写入float64的数据,不能读取
22 var ch2 <-chan int // 只能读取int型数据
23
24 // 关闭channel,直接调用close()即可
25 close(ch)
26 // 判断ch是否关闭,判断ok的值,如果是false,则说明已经关闭(关闭的话读取是不会阻塞的)
27 x, ok := <-ch
用法例子:
1 package main
2 import "fmt"
3
4 func print(ch chan int) {
5 fmt.Println("Hello world")
6 ch<- 1
7 }
8
9 func main() {
10 chs := make([]chan int)
11 for i := 0; i < 10; i++ {
12 chs[i] = make(chan int)
13 go print(chs[i])
14 }
15
16 for _, ch := range(chs){
17 <-ch
18 }
19 }
1.9 defer, panic, recover
defer 英文原意: vi. 推迟;延期;服从 vt. 使推迟;使延期。
panic 英文原意:n. 恐慌,惊慌;大恐慌 adj. 恐慌的;没有理由的 vt. 使恐慌 vi. 十分惊慌
recover 英文原意: vt. 恢复;弥补;重新获得 vi. 恢复;胜诉;重新得球 n. 还原至预备姿势
简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。恢复以后从defer函数直接返回了,不会恢复到发生panic 的点了。
(1)defer就是用来添加函数结束时执行的语句,就是说在含有defer的函数执行后,调用一下defer 指定的函数。
例子:下面这个函数返回1.
func f() (result int) {
defer func()
{
result++
}()
return 0
}
(2)panic 是用来表示非常严重的不可恢复的错误的。在Go语言中这是一个内置函数,接收一个interface{}类型的值(也就是任何值了)作为参数。panic一般会导致程序挂掉(除非recover)。如果一个函数有defer,那么发生panic时,不会立刻向上传递panic,而是到defer那,等defer跑完再向上传递panic。
(3)Go语言提供了recover内置函数,前面提到,一旦panic,逻辑就会走到defer那,那我们就在defer那等着,调用recover函数将会捕获到当前的panic(如果有的话),被捕获到的panic就不会向上传递了,程序也就不会挂了。
例子:func DeferPanic() {
if err := recover(); err != nil {
log.Errorf("[panic]:%s, %s", err, debug.Stack())
}
}
func main(){
defer DeferPanic()
//业务逻辑代码
......
//构造一个panic
panic(1)//这个1会在DeferPanic()函数的err打印出
}
2 接口与方法
Go 语言把所有的具有共性的方法定义在一起,成为了另外一种数据类型即接口,定义接收此类型的函数,叫方法。
1、接口是什么?
接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。
2、什么时候用接口
Go语言中的接口是一些方法的集合(method set),它指定了对象的行为:如果它(任何数据类型)可以做这些事情,那么它就可以在这里使用。
3、用接口的好处,如下例函数dayinji(c printer)入参为接口类型,可以传入实现接口定义的所有类型变量。
package main
import "fmt"
type printer interface {
print()
}
type Integer int
func (a Integer) print() {
fmt.Printf(" I am a int %d, I implement interface printer",a)
}
type Stringer string
func (b Stringer) print() {
fmt.Printf("I am a string %s, I implement interface printe",b)
}
func dayinji(c printer){
c.print()
}
func main() {
//var i printer
var a Integer = 9
var s Stringer = "abc"
dayinji(a)
dayinji(s)
}
输出:I am a int 9, I implement interface printerI am a string abc, I implement interface printe
●如果定义一个空的接口type printer interface {},那函数入参就可以为任意类型了。
2.1 接口变量赋值
原则1:
看实现接口的类,在实现接口方法时,指向的是指针,还是非指针。如果该类型实现接口方法是指向非指针的,那么该类实例化赋值给接口变量时,需要采用struct类型。如果该类型实现接口方法是指向指针的,那么该类实例化赋值给接口变量时,需要采用该类型的指针来赋值。
例如:
package main
import "fmt"
type printer interface {
print()
}
type Integer int
type Integer2 int
type Stringer string
func (a Integer)print(){
fmt.Printf("I am int,value = %d\n",a)
}
func (a *Integer2)print(){
fmt.Printf("I am intptr,value = %v\n",*a)
}
func main() {
var value Integer = 100
var value2 Integer2 = 101
var a printer = value
var b printer = &value2
a.print()
b.print()
}
输出结果:
I am int,value = 100
I am intptr,value = 101
原则2:
如果接口中定义多种方法,一个类型实现其中一种方法是非指针,实现另一种方法是指针,那么给该接口变量赋值时,需要用类型实例的指针给该接口变量赋值。
例子:
type superPrinter interface {
print()
copy()
}
type Integer int
func (a Integer)print(){
fmt.Printf("I am int printer,value = %d\n",a)
}
func (a *Integer)copy(){
fmt.Printf("I am int copy, value = %d\n",*a)
}
func main() {
var value Integer = 100
var a superPrinter = &value
a.print()
a.copy()
}
原则3:
组合类型,一人实现一个方法。直接看例子
type superPrinter interface {
print()
copy()
}
type Integer int
type INT struct{
Integer
}
func (a Integer)print(){
fmt.Printf("I am int printer,value = %d\n",a)
}
func (a *INT)copy(){
fmt.Printf("I am int copy, value = %v\n",*a)
}
func main(){
var a superPrinter = &INT{100}
a.print()
a.copy()
}
输出结果:
I am int printer,value = 100
I am int copy, value = {100}
3 time
参考:https://studygolang.com/articles/1881
暂时遇到定时器的两种用法:
(1)解决 channel 操作的 Timeout 问题。这个不能做定时器用,因为每次执行time那个case,都会重新创建定时器,并注册到select上。
select {
case msg <- input:
case <-time.After(time.Second)
}
(
2
)创建定时器
func demo(input chan interface{}) {
t1 := time.NewTimer(time.Second * 5)
t2 := time.NewTimer(time.Second * 10)
for {
select {
case msg <- input:
println(msg)
case <-t1.C:
println("5s timer")
t1.Reset(time.Second * 5)
case <-t2.C:
println("10s timer")
t2.Reset(time.Second * 10)
}
}
}
4 sync WaitGroup
WaitGroup的用途:它能够一直等到所有(应该是你想等待的)的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成。
WaitGroup总共有三个方法:Add(delta int),Done(),Wait()。简单的说一下这三个方法的作用。
Add:添加或者减少等待goroutine的数量
Done:相当于Add(-1)
Wait:执行阻塞,直到所有的WaitGroup数量变成0
例子:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i = i + 1 {
wg.Add(1)
go func(n int) {
// defer wg.Done()
defer wg.Add(-1)
EchoNumber(n)
}(i)
}
wg.Wait()
}
func EchoNumber(i int) {
time.Sleep(3e9)
fmt.Println(i)
}
输出结果:0 1 2 3 4
5 fmt
golang 的fmt 包实现了格式化I/O函数,类似于C的 printf 和 scanf。
5.1常用占位符
type Human struct {
Name string
}
var people = Human{Name:"zhangsan"}
普通占位符
占位符 说明 举例 输出
%v 相应值的默认格式。 fmt.Printf("%v", people) {zhangsan},
%+v 打印结构体时,会添加字段名 fmt.Printf("%+v", people) {Name:zhangsan}
%#v 相应值的Go语法表示 fmt.Printf("#v", people) main.Human{Name:"zhangsan"}
%T 相应值的类型的Go语法表示 fmt.Printf("%T", people) main.Human
%% 字面上的百分号,并非值的占位符 fmt.Printf("%%") %
%t true 或 false。 Printf("%t", true) true
%b 二进制表示 Printf("%b", 5) 101
%d 十进制表示 Printf("%d", 0x12) 18
%o 八进制表示 Printf("%d", 10) 12
%x 十六进制表示,字母形式为小写 a-f Printf("%x", 13) d
%X 十六进制表示,字母形式为大写 A-F Printf("%x", 13) D
%p 十六进制表示,前缀 0x Printf("%p", &people) 0x4f57f0
5.2 常用的方法
(1)输出到指定buf中
// Fprintf 将参数列表 a 填写到格式字符串 format 的占位符中并将填写后的结果写入 w 中,返回写入的字节数
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
(2)直接输出到屏幕。
// Printf 将参数列表 a 填写到格式字符串 format 的占位符中
// 并将填写后的结果写入 os.Stdout 中,返回写入的字节数
func Printf(format string, a ...interface{}) (n int, err error)
(3)格式化字符串
// Sprintf 将参数列表 a 填写到格式字符串 format 的占位符中
// 返回填写后的结果
func Sprintf(format string, a ...interface{}) string
(4)构造error类型
// Errorf 将参数列表 a 填写到格式字符串 format 的占位符中
// 并将填写后的结果转换为 error 类型返回
func Errorf(format string, a ...interface{}) error
6 接口查询
6.1什么是接口查询
接口查询,就是在一个接口变量中,去判断,那个把值赋给接口变量的对象,究竟有没有实现另一个接口,这个所谓的另一个接口,就是我要查询的那个接口。在这个已知的接口变量中,查询另一个接口的过程,就是接口查询了。
6.2 接口查询作用
通过接口查询,我们可以操作这个还原赋值给这个接口变量的对象的更加全面的功能。如赋值给这个接口变量的对象,实现了很多个接口,当他把值赋给这个接口后,那么这个接口变量就只能操作这个接口定义的方法了,相当于这个对象的一些功能就丢失了。通过接口查询,我们来充分的挖掘这个对象的更多功能
6.3 例子
package main
import (
"fmt"
)
type R interface {
Read()
}
type W interface {
Write(name string)
}
type RW interface {
R
W
}
type log struct {
name []string
r int
}
func (t *log) Read() {
if len(t.name) > t.r {
fmt.Println(t.name[t.r])
t.r++
} else {
fmt.Println("empty")
}
}
func (t *log) Write(name string) {
t.name = append(t.name, name)
fmt.Println("wirte success.", t.name)
}
func main() {
var r R = &log{}
var w W = &log{}
w.Write("write first")
w.Write("write second")
r.Read()
r.Read()
val, ok := w.(RW)
if !ok {
fmt.Println("hi")
} else {
val.Read()
val.Read()
}
}
go 源码学习
(1)进程编译过程中,编译器会在main函数之前加一堆的引导程序。
(2)在执行用户定义的main.main之前会先执行init的函数,被引用的包,lib,公共库中的init函数。所有的init函数都在同一个goroutine中执行,所有的init函数结束后才执行main.main函数