标签 Golang 下的文章

概念

panicrecover是Go的两个内置函数,这两个内置函数用于处理Go运行时的错误,panic用于主动抛出错误,recover用来捕获panic抛出的错误。

3441746ab2b9d09.jpg

  • 引发panic有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。
  • 发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印 函数调用堆栈,直到被recover捕获或运行到最外层函数。
  • panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panicdefer里面的panic能够被后 续执行的defer捕获。
  • recover用来捕获panic,阻止panic继续向上传递。recover()defer一起使用,但是defer只有在后面的函数体内直接 被掉用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。

例子1

//以下捕获失败
defer recover()
defer fmt.Prinntln(recover)
defer func(){
    func(){
        recover() //无效,嵌套两层
    }()
}()

//以下捕获有效
defer func(){
    recover()
}()

func except(){
    recover()
}
func test(){
    defer except()
    panic("runtime error")
}

例子2

多个panic只会捕捉最后一个:

package main
import "fmt"
func main(){
    defer func(){
        if err := recover() ; err != nil {
            fmt.Println(err)
        }
    }()
    defer func(){
        panic("three")
    }()
    defer func(){
        panic("two")
    }()
    panic("one")
}

使用场景

一般情况下有两种情况用到:

  1. 程序遇到无法执行下去的错误时,抛出错误,主动结束运行
  2. 在调试程序时,通过panic来打印堆栈,方便定位错误

其他要说的

go panic与recover分析及错误处理:https://blog.csdn.net/a41888313/article/details/79691728

概念

闭包是由函数及其相关引用环境组合而成的实体,一般提供通过在匿名函数中引用外部函数的局部变量或包全局变量构成。
即:闭包 = 函数 + 引用环境
闭包对闭包外的环境引入是直接引用,编译器检测到闭包,会将闭包引用的外部变量分配到栈上。
如果函数返回的闭包引用了该函数的局部变量(参数或函数内部变量):

  1. 多次调用该函数,返回的多个闭包所引用的外部变量是多个副本,原因是每次调用函数都会为局部变量分配内存
  2. 用一个闭包函数多次,如果该闭包修改了其引用的外部变量,则每一次调用该闭包对该外部变量都有影响,因为闭包函数共享外部引用
package main
func test(a int) func(i int) int{
    return func(i int) int{
        println(&a,a)
        a = a + i
        return a
    }
}
func main(){
    //f,g引用的外部的闭包环境包括本次函数调用的形参a的值1
    f := test(1)
    g := test(1)
    //多次调用的f,g引用的a为同一个a
    println(f(1))
    println(f(1))
    println(g(1))
    println(g(1))
}
//output
0x103100c0 1
2
0x103100c0 2
3
0x103100c4 1
2
0x103100c4 2
3

建议

如果一个函数调用返回的闭包引用修改了全局变量,则每次调用都会影响全局变量。
如果函数返回的闭包引用的是全局变量 a ,则多次调用该函数的返回的多个闭包引用的都是同一个 a 。同理,调用一个闭包多次引用一个 a 。此时如果闭包中修改了 a 值的逻辑,则每次闭包调用都会影响全局变量 a 的值。使用闭包是为了减少全局变量,所以闭包引用 全局变量不是好的编程方式。

 package main
 var a = 0
 func test() func(i int) int{
     return func(i int) int{
         println(&a,a)
         a = a + i
         return a
     }
 }
 func main(){
     f := test()
     g := test()
     //此时f,g引用的闭包环境中的a是同一个
     println(f(1))
     println(g(1))
     println(g(1))
     println(g(1))
     println(f(1))
 }
//output
 0xa9e9c 0
 1
 0xa9e9c 1
 2
 0xa9e9c 2
 3
 0xa9e9c 3
 4
 0xa9e9c 4
 5

价值

闭包最初的目的是减少全局变量,在函数调用的过程中隐式的传递共享变量,有其有用的一面;但是这种隐瞒的共享变量的方式带来的 坏处是不够直接,不够清晰,除非是非常有价值的地方,一般不会使用闭包。
对象是附有行为的数据,而闭包是附有数据的行为,类在定义是就已经显式的集中定义了行为,但是闭包中的数据没有显式的集中 声明的地方,这种数据和行为耦合的模型不是一种推荐的编程模型,闭包只是锦上添花的东西,不是必不可少的。
——《Go 语言核心编程》

其他要说的

什么是闭包?闭包的优缺点?:https://www.cnblogs.com/cxying93/p/6103375.html

函数签名

函数签名

函数类型又叫函数签名,一个函数的类型就是函数定义首行去掉函数名,参数名和{,可以使用fmt.Printf("%T",func)

package main
import "fmt"
func test(a int) int{
    a++
    return a
}
func main(){
    fmt.Printf("%T",test)
}
//output:
func(int) int

特点

  1. 可以用type来定义函数类型,函数类型可以作为函数的参数或返回值。
  2. 函数可以赋值与变量,可以使函数绑定一个变量,通过变量来调用
func test(a int) int{
    a++
    return a
}
func main(){
    t := test
    println(t(1))
}

匿名函数

顾名思义,没有函数名的函数,可直接赋值给变量,可作为实参,也可作为返回值,亦可直接调用。

package main
var moha = func(hath,hath2 int) int{
    return hath + hath2
}
func refunc() func(int)int{
    return func(a int) int{
        a++
        return a
    }
}
func infunc(f func(int) int) int{
    return f(90,2)
}
func main(){
    println(moha(1,2)) //直接调用
    println(refunc()(1)) //调用返回的函数
    println(infunc(moha)) //传入函数
}

延迟调用

defer

Go中提供defer来提供延迟调用,可以注册多个延迟调用,这些延迟调用遵循FILO(先进后出),顺序在函数返回前被执行。defer常用于保证一些资源最终一定能得到回收和释放,比如文件读写。

package main
func main(){
    defer func(){
        println("three")
    }()
    defer func(){
        println("two")
    }()
println("one")
}

特点

  1. FILO(先进后出)
  2. defer后必须是函数或方法的调用,不能为语句,否则报错:expression in defer must be function call
  3. defer函数的实参在注册时通过值拷贝传递进去,后续的操作语句不会对defer的函数造成影响
package main
func main(){
    a := 1
    defer func(a int){
        println(a)
    }(a) //值拷贝
    a++
    println(a)
}
  1. defer只有注册后才能执行,即在return前有效,在return后因为没有被注册而不会执行
  2. 主动退出(os.Exit(int))defer不会被执行
  3. defer最好不要调用有名函数,否则会产生一些奇奇怪怪的错误
  4. defer一般放在错误检查语句后
  5. 最好不要放在循环里

其他要说的

Go 中 defer 的 5 个坑:https://studygolang.com/articles/12061?fr=sidebar

定义

func funcName(param-list)(result-list){
    //code
}

特点

  1. 可无输入输出参数
  2. 多个相邻的相同类型参数可简写

    func test(a int,b int){ ...
    func test(a,b int){ ...

  3. 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,命名返回值变量会被初始化为零值,最后的return可以不带参数名直接返回

    func add(a,b int)(sum int){
    sum = a + b
    return
    //等效于 return sum
    }

  4. 不支持默认值
  5. 不支持函数重载
  6. 不支持命名函数嵌套
  7. func xxx(){,这个{一定要与声明在同一行
  8. 当只有一个返回值时可省去括号

多值返回

与Python相同(或类似?)

func test()(int,int){
    return a,b
}
func main(){
    a,b := test()
}

实参与形参的传递

Go函数实参到形参的传递永远是值拷贝,有时函数调用后实参指向的值发生的改变,那是因为参数传递的是指针值的值拷贝,实参是一个指针变量,传递给形参的是这个指针变量的副本,二者指向同一地址。

不定参数

声明

func funcName(param ...type)

特点

  1. 所有不定参数的类型必须相同
  2. 不定参数必须是函数的最后一个参数
  3. 不定参数在函数内相当于切片,对切片的操作同样适合对不定参数的操作,比如遍历
  4. 切片可以作为参数传递给不变参数,切片名后要加“...”,例子见下面
  5. 形参为不定参数的函数和形参为切片的函数类型不相同
 func sum(arr ...int)(sum int){
    for i,v := range arr{
        sum += v
    }
    return
}
//传参
s := []int{1,2,3,4,5}
sum(s...)

其他要说的

按道理上面的某些代码应该有特殊样式,不过预览看来没有(不会Markdown)。

条件语句

if 语句

用法

if 条件 {
     //当条件为真时执行
}

if 条件 {
     //当条件为真时执行
} else {
     //当条件为假时执行
}

if 条件 {
     //当条件为真时执行
} else if 条件2 {
     //当条件2为真时执行
} else {
     //全部不满足时执行
}

特点

  1. if后的条件不需要用小括号括起来
  2. {必须放在行末,与if或if else放在一行
  3. if后可带一个简单的初始化语句,并以分号分割,所声明变量的作用域为整个if语句块(包括后面的分支)
  4. Go语言没有条件运算符
  5. 遇到return后直接返回,遇到break则跳过break下方的if语句块

编程哲学

  1. 尽量减少条件语句的复杂度,如果条件语句太多,太复杂,则建议放到函数里封装起来
  2. 尽量减少if语句的嵌套层次,通过重构代码来使代码变得扁平,便于阅读

switch 语句

用法

switch {
     case 条件:
         //执行语句
     case 条件:
         //执行语句
     default:
         //所有条件不满足时执行
}

特点

  1. 同if一样,switch后可带一个简单的初始化语句
  2. switch后的表达式也是可选的,如果没有表达式,则case的子句是一个布尔表达式,而不是一个值,就像多重if...else...语句
  3. switch可以任意支持相等比较运算的类型变量
  4. 通过fallthough来强制执行下一个case子句(不判断下一case分支的条件)
  5. switch支持default语句,当所有条件不支持时执行default中的语句,default的位置不影响switch的判断逻辑
  6. switch和.(type)结合可以进行类型的查询(这个以后会学到)

循环语句

for 语句

Go语言只有一个循环语句——for

用法

//1.类似C中的for语句
for init;condition;post { }
//2.类似C中的while语句
for condition { }
//3.死循环
for { }
//for也可以对数组,切片,字符串,map和通道的访问

标签与跳转

Go使用标签来标识一个语句的位置,用于goto,break,continue语句的跳转。

标签

Lable: Statement

goto

goto用来跳转到函数内指定的标签

goto Lable

特点

  1. 只能在函数内跳转
  2. goto不能跳过内部变量声明语句
  3. goto只能跳转到同级作用域或上层作用域内,不能跳转到内部作用域

break

break用于函数内跳出for,switch,select语句的执行,有两种使用格式:

  1. 单独使用,用于跳出break当前所在的for,switch,select语句的执行
  2. 和标签一起使用,用于跳出标签所标识的for,switch,select语句的执行,可用于跳出多重循环,但标签与break必须在同一个函数内

continue

continue用于跳出for循环的本次迭代,跳到for循环下一次迭代的post语句执行,也有两种使用格式:

  1. 单独使用,用于跳出continue当前所在的for循环的本次迭代
  2. 和标签一起使用,用于跳出标签所标识的for语句的本次迭代,但标签与continue必须在同一个函数内

切片

在Go中由于数组的定长性和值拷贝的特性限制了数组的使用场景,使用Go提供了另外一种数据类型slice(切片),这是一种变长数组。

声明

//由make创建
SliceName := make([]Type,Len,Cap)  //可不填写Cap,但会使Cap=Len
//由数组创建
ArrayName := [...]Type{value1,value2,value3}
SliceName := Array[b,e]  //b:开始索引 e:结束索引 都可不指定
//直接声明切片类型变量是没有意义的
var a []int  //打印结果:[]
//此时array=nil,len=0,cap=0

切片的操作

操作

//长度
len(Slice)
//底层数组容量
cap(Slice)
//添加元素
append(Slice,Value)
//复制切片
copy(Slice1,Slice2)

例子

package main
import "fmt"

func PrintSliceInfo(s1,s2 []int){
        fmt.Println("--------------")
        fmt.Println("s1:",s1)
        fmt.Println("s2:",s2)
        fmt.Println("s1 cap:",cap(s1))
        fmt.Println("s2 cap:",cap(s2))
        fmt.Println("s1 len:",len(s1))
        fmt.Println("s2 len:",len(s2))
        fmt.Println("--------------")
}
func main(){
        a := [...]int{0,1,2,3,4,5}
        s1 := make([]int,3,6)
        s2 := a[3:]
        PrintSliceInfo(s1,s2)
        s1 = append(s1,23)
        s2 = append(s2,233)
        PrintSliceInfo(s1,s2)
        s3 := make([]int,3,4)
        copy(s3,s2)
        fmt.Println("--------------")
        fmt.Println("s3:",s3)
        fmt.Println("s3 len:",len(s3))
        fmt.Println("s3 cap:",cap(s3))
        fmt.Println("--------------")
}
//打印结果
--------------
s1: [0 0 0]
s2: [3 4 5]
s1 cap: 6
s2 cap: 3
s1 len: 3
s2 len: 3
--------------
--------------
s1: [0 0 0 23]
s2: [3 4 5 233]
s1 cap: 6
s2 cap: 8
s1 len: 4
s2 len: 4
--------------
--------------
s3: [3 4 5]
s3 len: 3
s3 cap: 4
--------------

字典

声明

//通过字面量创建
MapName := map[Type1]Type2{"key":value,"key2",value2}
//通过Make创建
MapName := make(map[Type1]Type2)

用法

//添加与更新,访问一个键
MapName[key] = value
value := MapName[key]
//删除
delete(MapName,key)
//长度
len(MapName)
//迭代
for key,value := range MapName{
    //code
    fmt.Println("key=",key,"value=",value)
}

注意

  1. Go内置的Map不是并发安全的,并发安全可以使用标准包sync中的map
  2. 不要直接修改map value中的某个元素的的值,如果需要修改,则必须整体重新赋值
  3. 可以使用range迭代,但不保证每次的迭代顺序相同

结构

Go中的struct(结构)与C类似。

声明

//通过字面量,这种方法用的较少
struct{
FeildName  FeildType
FeildName  FeildType
FeildName  FeildType
}
//通过自定义类型
type TypeName struct{
FeildName  FeildType
FeildName  FeildType
FeildName  FeildType
}

示例

type test struct{
Name String
Age int
}
//不推荐下面的方式来初始化
a := test{"test",23}
//建议使用下面的方法,避免新增字段导致报错,没有赋值的字段默认为零值
c := test{
Name:"test",
Age:23,
}

访问

Value := TypeName.FeildName

注意

  1. struct中的类型可以是任意类型
  2. struct的存储空间是连续的,其字段按照声明时的顺序存放(注意字段之间对齐!)

其他要说的

切片的原理

请务必看一下,了解切片的工作原理!
https://www.cnblogs.com/hutusheng/p/5492418.html

变量

声明

在Go中这样来声明变量:

var Name Type [ = value]  //不指定默认值,默认为零值
var Name = [ = value]
var (
a int = 0
b string = "test"
)

或这样简单的声明,这种方法只能用在函数(或方法)中:

Name := value  //自动判断类型
Name1,Name2 := value1,value2

变量类型与命名规则请看上一篇文章。

存储与生存期

Go语言提供自动内存管理,通常不需要特别关注变量的生存期和存放位置。Go使用了栈逃逸技术能为变量自动分配空间:可能在栈上,有可能在堆上。
关于栈逃逸技术:https://studygolang.com/articles/10026

类型转换

在Go中,不同于js之类的语言,不同类型的变量要类型转换:

TypeName(expression)  //expression:表达式或变量名

例:

var a int = 1
var b int8 = 2

a = b  //非法
a = int(b)

常量

常量中的内容不可改变:

const Name [ = value/iota]

例:

const (Name1 = iota  //Name1 = 0
       Name2  //Name2 = 1
)
//关于iota请看上一篇文章

指针

Go支持指针,也支持多级指针,指针的声明类型为T(多级:*T),在变量名前加&来获取地址:

var a = 233
b := &a
println(a)
println(b)
println(*b)
//值打印结果
233
0x11f3bfb4  //内存地址
233

又一个例子:

package main
func test(a,b int) *int {
    c := a * b  //c的作用域在函数内
    println(&c)
    return &c
}
func main() {
    p := test(2,3)
    println(p)
}
//打印结果
0x1233bfb8
0x1233bfb8
//Go编译器使用栈逃逸使这种局部变量空间分配到栈上。

由于Go的垃圾回收机制,所以Go不支持指针运算。

数组

声明

var Name [Size]type

例:

arr := [3]int{1,2,3}
arr2 := []int{1,2,3,4,5,6,7}  //自动判断长度
arr3 := [3]int{1:1,2:3}  //指定默认值,没有指定的使用类型默认值
arr4 := []int{1:2,6:4}  //指定判断长度,由最大决定

特性

  1. 数组一旦创建就固定了,不能再追加元素
  2. 数组是值类型的,数组赋值或做为函数参数都是值拷贝
  3. 数组长度是数组的组成部分,[10]int != [20]int
  4. 可根据数组创建切片

元素访问

a := [...]int{1,2,3}
for index,value := range a {
    fmt.Println(value)
}

数组长度

len(Name)

range

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对的 key 值。

其他要说的

字符串运算

参考:

  1. https://blog.csdn.net/hatlonely/article/details/79156311
  2. https://www.cnblogs.com/yshyee/p/8810931.html

控制结构

啊?这么快就到for了?

标识符

标识符用来标示变量,类型,常量等语法对象的符号名称,在语法分析的时候作为一个token存在。(token会在文末解释)

Go 的关键字

Go只有25个关键字(当然,Go是一门极简的语言):

break  default  func  interface  select  case  defer  go  map  struch  chan  else  goto
package  switch  const  fallthrough  if  range  type  continue  for  import  return  var

数据类型与常量标识符

这些同样不可用作声明变量等:

数据类型

数值(16):
    整形(12):
        #int与uint的区别是有无符号,int(uint),int8(uint8)等的区别为bit数。
        byte  int  int8  int16  int32  int64
        uint  uint8  uint16  uint32 uint64 uintptr
    浮点(2):
        float32  float64
    复数(2):
        complex64  complex128
字符与字符串(2):
    string rune
接口型(1):
    error
布尔型(1):
    bool

常量

bool型: true false
常量计数器: iota
空: nil

空白标识符

声明匿名变量,占位标识等: _

标识符规则

标识符开头可以为“A-Z”,“a-z”以及“_”,标识符不能为纯数字(或数字开头),如以下例子:

aaaaa  //合法
aaaaa2  //合法
_aaaaa //合法
-aaaaa  //非法标识符
9aaaaa  //非法
_aaaaa2  //非法

运算符

Go语言有47个操作符,同样作为一个token存在:

算数运算符

+  -  *  /

位运算符

&  |  ^  &^  >>  <<

赋值与复核运算符

:=  =  +=  -=  *=  /=  %=  &=  |=  ^=  &^=  >>=  <<=

比较运算符

>  >=  <=  <  ==  !=

括号

(  )  {  }  [  ]

逻辑运算符

&&  ||  !

自增自减操作符

++  --

其他运算符

:  ,  ;  .  ...  <-

token

引用《Go语言核心编程》:

token是构成源程序的基本不可再分割的单元。编译器第一步就是将源程序分割为一个个独立的token,这个过程就是语法分析。Go语言的token可以分为关键字,标识符,操作符,分隔符和字面常量。

如下面个例子:

fmt.Printf("Hello World!\n")
这里有6个token
分别为:
fmt
.
Printf
(
"Hello World!\n"
)

iota

iota只能在常量表达式中使用,在枚举中应用广泛:

const (
a = iota
b
c
)
你可以试着打印这几个常量的值

9_0.png

其他要说的

运算符资料

  1. https://www.jb51.net/article/129776.htm
  2. https://studygolang.com/articles/12463

数据类型

  1. https://blog.csdn.net/qq_31179577/article/details/81486883