go(01) 基础语法

索引 1. 数据类型 2.变量 3.结构体 4.运算符 5.条件 6.循环 7.函数 8.数组 9.指针 10.切片 11.Range(范围) 12.Map 13.递归 14.类型转换 15.接口 16.并发 17.错误及异常

go的语句结尾不需要分号(;)  遇到换行就代表一个语句的结束

1.数据类型 基本类型有:

  • bool
  • string
  • int、int8、int16、int32、int64
  • uint、uint8、uint16、uint32、uint64、uintptr
  • byte // uint8 的别名
  • rune // int32 的别名 代表一个 Unicode 码
  • float32、float64
  • complex64、complex128 

2.变量的声明和定义  var是声明变量的关键字

  变量声明的标准格式:   var 变量名 变量类型  变量声明以关键字 var 开头,后置变量类型,行尾无须分号

       批量格式: 

var ( a int
    b string
    c []int)

  简短格式: 名字 := 表达式  如:

func main() {
    x:=100
    a,s:=1, "abc"
}
需要注意的是,简短模式(short variable declaration)有以下限制:
    • 定义变量,同时显式初始化。
    • 不能提供数据类型。
    • 只能用在函数内部。

  因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。var 形式的声明语句往往是用于需要显式指定变量类型地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

  := 只能在函数里使用,在全局无法编译, 所以一般使用var的方式来定义全局变量

  注意:由于使用了 :=,而不是赋值的 =,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

     no new variables on left side of :=  在“:=”的左边没有新变量出现,意思就是“:=”的左边变量已经被声明了。

     但是 = 还是可以的,因为这是为变量赋值而非定义

多变量声明  vname1, vname2, vname3 = v1, v2, v3


  变量的初始化标准格式   var 变量名 类型 = 表达式 比如 var hp int = 100

  编辑器推到类型的格式  var hp = 100 在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型。

 空标识符  如果一个字段在代码中从来不会被用到,那可以把它命名为 _,即空标识符

  它可以用来避免为某个变量起名 同时也可以在赋值时 舍弃某个值

  在函数中的 使用 

package main

import "fmt"

func main() {
  _,numb,strs := numbers() //只获取函数返回值的后两个
  fmt.Println(numb,strs)
}

//一个可以返回多个值的函数
func numbers()(int,int,string){
  a , b , c := 1 , 2 , "str"
  return a,b,c
}
输出结果:
2 str

  

3.结构体的定义  引自 : https://www.cnblogs.com/sparkdev/p/10761825.html

  结构体的定义格式如下

    type 类型名 struct {
        字段1 字段1的类型
        字段2 字段2的类型
         …
     }

  结构体中的字段可以是任何类型,甚至是结构体本身,也可以是函数或者接口。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值:

type Person struct {  定义结构体类型
    Name  string
    Age     int
    Email string
}
var p Person       声明结构体
p.Name = "nick"     给结构体的成员变量赋值
p.Age = 28

结构体的初始化及访问方法

package main

import "fmt"

type Person struct{
Name string
Age int
}

func main() {
person := Person {"李四",18}
fmt.Printf("%s , %d ",person.Name , person.Age)
}

package main

import "fmt"

type Person struct{
    Name string
    Age     int
}

func main() {
    person := Person {"李四",18}
    printPerson(person)
}
func printPerson (person Person){
    fmt.Printf("%s , %d
",person.Name , person.Age)
}
结构体作为函数形参
package main

import "fmt"

type Person struct{
    Name string
    Age     int
}

func main() {
    person := Person {"李四",18}
    printPerson(&person)
}
func printPerson (person *Person){
    fmt.Printf("%s , %d
",person.Name , person.Age)
结构体指针

4.GO的运算符和C没什么分别

5.GO的条件语句 

package main

import "fmt"

func main() {
   /* 局部变量定义 */
   var a int = 100;
 
   /* 判断布尔表达式 */
   if a < 20 {
       /* 如果条件为 true 则执行以下语句 */
       fmt.Printf("a 小于 20
" );
   } else {
       /* 如果条件为 false 则执行以下语句 */
       fmt.Printf("a 不小于 20
" );
   }
   fmt.Printf("a 的值为 : %d
", a);

}
以上代码执行结果为:

a 不小于 20
a 的值为 : 100
if else语句
package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90

   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
   }

   switch {
      case grade == "A" :
         fmt.Printf("优秀!
" )    
      case grade == "B", grade == "C" :
         fmt.Printf("良好
" )      
      case grade == "D" :
         fmt.Printf("及格
" )      
      case grade == "F":
         fmt.Printf("不及格
" )
      default:
         fmt.Printf("" );
   }
   fmt.Printf("你的等级是 %s
", grade );      
}
以上代码执行结果为:

优秀!
你的等级是 A
switch

 使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。

package main

import "fmt"

func main() {

    switch {
    case false:
            fmt.Println("1、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("2、case 条件语句为 true")
            fallthrough
    case false:
            fmt.Println("3、case 条件语句为 false")
            fallthrough
    case true:
            fmt.Println("4、case 条件语句为 true")
    case false:
            fmt.Println("5、case 条件语句为 false")
            fallthrough
    default:
            fmt.Println("6、默认 case")
    }
}
以上代码执行结果为:

2case 条件语句为 true
3case 条件语句为 false
4case 条件语句为 true
fallthrough

  select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。

  select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

以下描述了 select 语句的语法:

    • 每个 case 都必须是一个通信
    • 所有 channel 表达式都会被求值
    • 所有被发送的表达式都会被求值
    • 如果任意某个通信可以进行,它就执行,其他被忽略。
    • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
      否则:
      1. 如果有 default 子句,则执行该语句。
      2. 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
package main

import "fmt"

func main() {
   var c1, c2, c3 chan int
   var i1, i2 int
   select {
      case i1 = <-c1:
         fmt.Printf("received ", i1, " from c1
")
      case c2 <- i2:
         fmt.Printf("sent ", i2, " to c2
")
      case i3, ok := (<-c3):  // same as: i3, ok := <-c3
         if ok {
            fmt.Printf("received ", i3, " from c3
")
         } else {
            fmt.Printf("c3 is closed
")
         }
      default:
         fmt.Printf("no communication
")
   }    
}
以上代码执行结果为:

no communication
select

6.GO的循环语句

  Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。

  和 C 语言的 for 一样:

  for init; condition; post { }

  和 C 的 while 一样:

  for condition { }

  和 C 的 for(;;) 一样:

  for { }
  • init: 一般为赋值表达式,给控制变量赋初值;
  • condition: 关系表达式或逻辑表达式,循环控制条件;
  • post: 一般为赋值表达式,给控制变量增量或减量。

具体实例参考 :https://www.runoob.com/go/go-loops.html


7.GO函数

  func [函数名] (函数形参,...) 返回类型 { 函数体 }

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200
   var ret int

   /* 调用函数并返回最大值 */
   ret = max(a, b)

   fmt.Printf( "最大值是 : %d
", ret )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
   /* 定义局部变量 */
   var result int

   if (num1 > num2) {
      result = num1
   } else {
      result = num2
   }
   return result
}
以上实例在 main() 函数中调用 max()函数,执行结果为:

最大值是 : 200
函数调用
package main

import "fmt"

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b)
}
以上实例执行结果为:

Runoob Google
函数多个返回值
Go 语言可以很灵活的创建函数,并作为另外一个函数的实参。以下实例中我们在定义的函数中初始化一个变量,该函数仅仅是为了使用内置函数 math.sqrt(),实例为:

实例
package main

import (
   "fmt"
   "math"
)

func main(){
   /* 声明函数变量 */
   getSquareRoot := func(x float64) float64 {
      return math.Sqrt(x)
   }

   /* 使用函数 */
   fmt.Println(getSquareRoot(9))

}
以上代码执行结果为:

3
函数作为另一个函数的实参

   闭包的概念是 在函数A里创建 匿名函数B , 当A定义一个局部变量,一般情况下该变量会随着函数A的结束而释放, 但如果这时 B 调用了该变量, 函数A结束时该变量仍会存在于内存中, 当再次调用 A 时函数内部定义的局部变量 仍会是上一次调用的变量

  闭包的作用 , 可以让临时变量在外部函数生命周期结束时,仍然存在于内存之中!

  闭包的弊端 ,由于闭包会使函数中的变量保存在内存中,内存消耗很大,所以不能滥用闭包,解决办法是,退出函数之前,将不使用的局部变量删除。

package main

import "fmt"

func testfunc() func() int{
    i:=0
    return func() int{
        i+=1
        return i
    }
}


func main() {
    x := testfunc() //x是一个函数 因为:=的特殊 会根据右值来定义左值的类型
    fmt.Println(x())
    fmt.Println(x())
    fmt.Println(x())   
}
闭包

8.GO数组

  数组其实就是一个指向一块连续地址的指针

  声明数组 var variable_name [SIZE] variable_type

  初始化数组与访问的方法和C无异, 注意如果数组的value不是在声明时初始化, 那之后只能通过访问数组下标的方式赋值

package main

import "fmt"

func main() {
    var arr = [3]int {0,1,2}
    //也可 arr := [3]int {0,1,2}
    for i:=0;i<3;i++{
        fmt.Printf("%d 
", arr[i])
    }
}

  二维数组的定义及访问

package main

import "fmt"

func main() {
    var arr [3][4]int
    for i:= 0; i<3; i++{
        for j:=0; j<4; j++{
            arr[i][j] = j
        }
    }
    for i:= 0; i<3; i++{
        for j:=0; j<4; j++{
            fmt.Printf("%d
",arr[i][j])
        }
    }
}
执行结果
0
1
2
3
0
1
2
3
0
1
2
3
View Code

  向函数传递数组


func func_name (praams []int ){ ... }  不指明数组长度

func func_name (praams [len]int ){ ... } 指明数组长度

var array = []int{1, 2, 3, 4, 5}
    /* 未定义长度的数组只能传给不限制数组长度的函数 */
setArray(array)
    /* 定义了长度的数组只能传给限制了相同数组长度的函数 */
var array2 = [5]int{1, 2, 3, 4, 5}

9.GO指针

  定义指针 var var_name *var-type

   指针使用和C的无异 

package main

import "fmt"

func main() {
    a := 10
    p:= &a
    fmt.Printf("指针指向的值%d
指针本身的地址%x
指针指向的地址%x
",*p,p,&p)
}
运行结构

指针指向的值10
指针本身的地址c0000160a0
指针指向的地址c00000e028

  指针数组 var ptr [len]*int

package main

import "fmt"

const MAX int = 3

func main() {
   a := []int{10,100,200}
   var i int
   var ptr [MAX]*int;

   for  i = 0; i < MAX; i++ {
      ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
   }

   for  i = 0; i < MAX; i++ {
      fmt.Printf("a[%d] = %d
", i,*ptr[i] )
   }
}
以上代码执行输出结果为:

a[0] = 10
a[1] = 100
a[2] = 200
指针数组

  二级指针 var ptr1 *int a var ptr2 **int b  b = &a

package main

import "fmt"

func main() {

   var a int
   var ptr *int
   var pptr **int

   a = 3000

   /* 指针 ptr 地址 */
   ptr = &a

   /* 指向指针 ptr 地址 */
   pptr = &ptr

   /* 获取 pptr 的值 */
   fmt.Printf("变量 a = %d
", a )
   fmt.Printf("指针变量 *ptr = %d
", *ptr )
   fmt.Printf("指向指针的指针变量 **pptr = %d
", **pptr)
}
以上实例执行输出结果为:

变量 a = 3000
指针变量 *ptr = 3000
指向指针的指针变量 **pptr = 3000
二级指针

  指针作为函数的形参

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int= 200

   fmt.Printf("交换前 a 的值 : %d
", a )
   fmt.Printf("交换前 b 的值 : %d
", b )

   /* 调用函数用于交换值
   * &a 指向 a 变量的地址
   * &b 指向 b 变量的地址
   */
   swap(&a, &b);

   fmt.Printf("交换后 a 的值 : %d
", a )
   fmt.Printf("交换后 b 的值 : %d
", b )
}

func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址的值 */
   *x = *y      /* 将 y 赋值给 x */
   *y = temp    /* 将 temp 赋值给 y */
}
以上实例允许输出结果为:

交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100
swap
package main

import "fmt"

func main() {
    /* 定义局部变量 */
   var a int = 100
   var b int= 200
   swap(&a, &b);

   fmt.Printf("交换后 a 的值 : %d
", a )
   fmt.Printf("交换后 b 的值 : %d
", b )
}

/* 交换函数这样写更加简洁,也是 go 语言的特性,可以用下,c++ 和 c# 是不能这么干的 */
 
func swap(x *int, y *int){
    *x, *y = *y, *x
}
更简洁的swap

 10.GO 切片(slice)

   切片类似于动态数组, 长度可追加,

  定义切片有三种:

    1.声明一个未指定长度的数组  var identifier []type 

    2.通过内置函数make()创建切片 var slie_name [ ]type = make([ ]type , len , [cap])

          []type是数据类型 len是切片长度(长度不可大于容量否则报错) cap是切片容量(可选参数,如果不给则默认等于len)

    3.通过引用已经存在的数组创建  var arr = [6]int {0,1,2,3,4,5}

      slice := arr[start_index:end_index-1] 引用arr下标从start_index到end_index-1的的元素作为初始化切片的值

      slice := arr[1:3] slice未指定长度所以是切片 , 引用arr数组的下标1到下标3-1的两个元素来初始化新创建的切片

      s := arr[start_index:]  默认从start_index开始一直到数组的最后一个元素

      s := arr[:end_index-1]  默认从第一个元素开始一直到指定的下标为end_index-1结束

  如果创建切片时未初始化那么切片默认是空的, len=0 , cap=0 , slice=nil

  切片有两个内置函数 len(slice) 和cap(slice) , len获取切片的长度 , cap获取切片的容量

  append()和copy()

    如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v
",len(x),cap(x),x)
}
以上代码执行输出结果为:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
append和copy的方法

 11.GO Range

  Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。

  range关键字会返回两个值(索引和索引对应的值), 可以用 _(空标识符)来舍弃不需要的值

package main
import "fmt"
func main() {
    arr := []int {0,1,2,3,4}
    for n,v := range arr{
        fmt.Println("index:",n)
        fmt.Println("value:",v)
    }
}
执行结果

index: 0
value: 0
index: 1
value: 1
index: 2
value: 2
index: 3
value: 3
index: 4
value: 4


也可 用于map的遍历

package main

import "fmt"

func main() {
  kvs := map[int]string{1:"hello",2:"world",3:"!"}
  for k,v:= range kvs{
    fmt.Printf("[%d]%s ",k,v)
  }
}

执行结果

[1]hello
[2]world
[3]!


12.GO Map

  声明map可以使用map关键字也可以使用内置函数make , 声明map默认是nil  

var map_name map[key_type]value_type 只声明默认是nil
map_name := make([key_type]value_type)
map插入key和value 
var map_name [int]string
map_name [1] = "hello"
map_name [2] = "world"
map_name [3] = "!"
定义map并初始化 type_map := map[int]string {1:"a",2:"b",3:"c"}

  delete() 用于删除集合元素

  delete(map_name,map_key) 删除指定map里的指定索引值代表的value


13.GO递归函数

  递归函数就是在运行的过程中调用自己。

  Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。

  递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。


14.GO类型转换

 基本格式 type_name(expression)  type_name为要转换的类型 , expression 为表达式 , 转换仅对当次调用有效, 不会根本的改变元素原本的类型

package main

import "fmt"

func main() {
   var sum int = 17
   var count int = 5
   var mean float32
   
   mean = float32(sum)/float32(count)
   fmt.Printf("mean 的值为: %f
",mean)
}
以上实例执行输出结果为:

mean 的值为: 3.400000
实例

 15. GO 接口

  Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任意类型只要实现了这些方法就是实现了这个接口。

package main

import "fmt"

type Human struct{
    name    string
    age        int
    sex        string
}

type Zoon struct{
    speices string
    sex        string
}

type iface interface{    定义一个接口
    call()
}

func (man Human) call(){    接口方法1
    fmt.Println(man.name,man.sex,man.age)
}

func (doge Zoon) call(){    接口方法2
    fmt.Println(doge.speices,doge.sex)
}

func main() {
    var p iface
    p = Human{"张三",20,""}    //结构体赋值接口
    p.call()
    p = Zoon{"","雄性"}
    p.call()
}
接口定义及方法

  接口是泛式的, 只有实现接口方法不同的区别


16.GO 并发

  Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。

  goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

goroutine的格式 go 函数名( 参数列表 )
例如: go f (x int, y string)

  Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。

package main

import (
        "fmt"
        "time"
)

func say(s string) {
        for i := 0; i < 5; i++ {
                time.Sleep(100 * time.Millisecond)
                fmt.Println(s)
        }
}

func main() {
        go say("world")
        say("hello")
}
执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行:

world
hello
hello
world
world
hello
hello
world
world
hello
实例

通道(channel)

  通道(channel)是用来传递数据的一个数据结构。

  通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

  声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建: ch := make(chan int)

  注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

  以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:

实例
package main

import "fmt"

func sum(s []int, c chan int) {
        sum := 0
        for _, v := range s {
                sum += v
        }
        c <- sum // 把 sum 发送到通道 c
}

func main() {
        s := []int{7, 2, 8, -9, 4, 0}

        c := make(chan int)
        go sum(s[:len(s)/2], c)
        go sum(s[len(s)/2:], c)
        x, y := <-c, <-c // 从通道 c 中接收

        fmt.Println(x, y, x+y)
}
输出结果为:

-5 17 12
实例

  通道缓冲区

  通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小: ch := make(chan int, 100)

  带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

  不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

  注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
        // 缓冲区大小为2
        ch := make(chan int, 2)

        // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
        // 而不用立刻需要去同步读取数据
        ch <- 1
        ch <- 2

        // 获取这两个数据
        fmt.Println(<-ch)
        fmt.Println(<-ch)
}
执行输出结果为:

1
2
实例

  Go 遍历通道与关闭通道

package main

import(
    "fmt"
    "time"
)

func twrite(c chan int,n int){
    for i:=0; i<n; i++{
        c <- i
    }
    //如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
    close(c)    //写入10个数据后关闭通道
}

func main() {
    c := make(chan int,10)    //创建通道及缓冲区大小
    go twrite(c,cap(c))
    time.Sleep(time.Duration(3)*time.Second)
    fmt.Println("休眠3秒后")
    
    for i:=range c{
        //range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        //数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        //之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不会结束,
        //从而在接收第 11 个数据的时候出错(fatal error: all goroutines are asleep - deadlock!)线程死锁。
        fmt.Println(i)
    }

    fmt.Println("缓冲区数据全部读取后再次访问通道")
    for i:=range c{
        //通道已经关闭,所有无法进入for
        fmt.Println(i,"1")
    }    
}
执行结果:
休眠3秒后
0
1
2
3
4
5
6
7
8
9
缓冲区数据全部读取后再次访问通道
实例

17.GO错误处理及异常 转自网络(几篇一样的我也不晓得谁是原创)

  错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中 ;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是

   Golang中引入error接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含error。error处理过程类似于C语言中的错误码,可逐层返回,直到被处理。

  Golang中引入两个内置函数panic()和recover()来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
  当程序运行时,如果遇到引用空指针、下标越界或显式调用panic()函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。如果一路在延迟函数中没有recover函数的调用,则会到达该携程的起点,该携程结束,然后终止其他所有携程,包括主携程(类似于C语言中的主线程,该携程ID为1)。

  错误和异常从Golang机制上讲,就是error和panic的区别。很多其他语言也一样,比如C++/Java,没有error但有errno,没有panic但有throw。

 Golang错误和异常是可以互相转换的:

  1. 错误转异常,比如程序逻辑上尝试请求某个URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。
  2. 异常转错误,比如panic触发的异常被recover恢复后,将返回值中error类型的变量进行赋值,以便上层函数继续走错误处理流程。

  errors.New(string) 将string类型的错误信息转换为 error 类型的错误信息并返回

  panic(error) 打印错误信息并终止程序   引发panic有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。

  recover()  可以捕获异常并返回

  defer 在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完 毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)。当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的(defer栈)当函数执行完毕后,再从defer栈 安装先入后出的方式出栈执行

  defer 一般用在函数结束时释放资源, 和处理错误异常

package main

import "fmt"

func main() {
    myPainc()
}
func myPainc() {
    y := 10
        defer fmt.Println(9)
    defer func(x int){
        fmt.Println(x)
    }(y)    //这个括号是用于立即调用匿名函数并传参的
}
输出结果是
10
9

defer的用法
defer的用法
package main
import(
    "fmt"
    "errors"
)

var ERR_READ_FILE = errors.New("读取文件错误")

func ReadFile(fileName string) error {
    if fileName == "file.ini"{
        return nil
    }else{
        return ERR_READ_FILE
    }
}

func main(){
    er := ReadFile("file")
    if er != nil {
        panic(er)
    }
    fmt.Println("无错误程序继续执行")
}
自定义错误处理
package main

import "fmt"

func main() {
    defer func() {                                //结束程序前会执行defer
        if err := recover(); err != nil {        //recover()会捕获 panic 抛出的error
            fmt.Println("出了错:", err)            //接收 recover 捕获的 error 并处理
        }
    }()
    myPainc()
    fmt.Printf("这里应该执行不到!")
}
func myPainc() {
    panic("我就是一个大错误!")    //抛出error打印错误信息并结束程序
}
recover()和defer的组合使用(处理抛出的异常)

 错误处理的正确姿势

  1.错误原因只有一个时,不使用err ,应该使用bool

func (self *AgentContext) CheckHostType(host_type string) error {
    switch host_type {
    case "virtual_machine":
        return nil
    case "bare_metal":
        return nil
    }
    return errors.New("CheckHostType ERROR:" + host_type)
}

使用bool

func (self *AgentContext) IsValidHostType(hostType string) bool {
    return hostType == "virtual_machine" || hostType == "bare_metal"
}

  2.没有错误时不要使用err

func (self *CniParam) setTenantId() error {
    self.TenantId = self.PodNs
    return nil
}

重构一下后

func (self *CniParam) setTenantId() {
    self.TenantId = self.PodNs
}

  3.error应该放在返回值类型列表的最后一个

resp, err := http.Get(url)
if err != nil {
    return nill, err
}

  4.error应该统一定义而不是随意返回

var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")

  5.错误逐层返回时,层层都加日志 层层都加日志非常方便故障定位。说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到。如果你或你的团队能做到,那么请忽略这个姿势

  6.错误处理使用defer和recover()

  7.当尝试几次可以避免失败时, 不要立刻返回错误  

    如果错误的发生是偶然性的,或由不可预知的问题导致。一个明智的选择是重新尝试失败的操作,有时第二次或第三次尝试时会成功。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。

    两个案例:

      1.我们平时上网时,尝试请求某个URL,有时第一次没有响应,当我们再次刷新时,就有了惊喜。

      2.团队的一个QA曾经建议当Neutron的attach操作失败时,最好尝试三次,这在当时的环境下验证果然是有效的。

  8.当上层函数不关心错误时 , 建议不返回error 对于一些资源清理相关的函数(destroy/delete/clear),如果子函数出错,打印日志即可,而无需将错误进一步反馈到上层函数,因为一般情况下,上层函数是不关心执行结果的,或者即使关心也无能为力,于是我们建议将相关函数设计为不返回error。

  9.当发生错误时,不要忽略有用的返回值  通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,应该将读取到的字符串和错误信息一起打印出来。说明:对函数的返回值要有清晰的说明,以便他人使用

  异常处理的正确姿势

  1.在程序开发阶段,坚持 "速错" 简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。

  2.在程序部署后,应恢复异常避免程序终止 一旦Golang程序部署后,在任何情况下发生的异常都不应该导致程序异常退出,我们在上层函数中加一个延迟执行的recover调用来达到这个目的,并且是否进行recover需要根据环境变量或配置文件来定,默认需要recover。
   我们在调用recover的延迟函数中以最合理的方式响应该异常:

    1.打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;

    2.将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。

  3.对于 不应该出现的分支,使用异常处理  当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径:

switch s := suit(drawCard()); s {
    case "Spades":
    // ...
    case "Hearts":
    // ...
    case "Diamonds":
    // ... 
    case "Clubs":
    // ...
    default:
        panic(fmt.Sprintf("invalid suit %v", s))
}

  4.针对入参不应该有问题的函数,使用panic设计


原文地址:https://www.cnblogs.com/yxnrh/p/13876619.html