目录
GO语言基础
/    

GO语言基础

变量和常量

1. 如何定义变量

单声明变量

var名称类型是声明单个变量的语法。

  1. 第一种,指定变量类型,声明后若不赋值,使用默认值
var name string
name = "a"
  1. 第二种,根据值自行判定变量类型(类型推断Type inference)
    如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型。
var name ="a"
  1. 第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误(简短声明)
var b = 10
c : = 10

这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值

package main

var a = "a"

var b string = "b"
var c bool

func main(){
    println(a, b, c)
}

多变量声明

  1. 第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值
    

    var name1, name2, name3 type
    name1, name2, name3 = v1, v2, v3
    
  2. 第二种,直接赋值,下面的变量类型可以是不同的类型

    var name1, name2, name3 = v1, v2, v3
    
  3. 第三种,集合类型

    var (
       name string
       age int
    )
    

注意:

●变量必须先定义才能使用
●go语言是静态语言,要求变量的类型和赋值的类型必须一致。
●变量名不能冲突。(同一个作用于域内不能冲突)
●简短定义方式,左边的变量名至少有一个是新的
●简短定义方式,不能定义全局变量。
●变量的零值。也叫默认值。
●变量定义了就要使用,否则无法通过编译。

如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。

如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:

func main() {
    var name string = "a"
    fmt.Println("a, b")
}

尝试编译这段代码将得到错误 a declared and not used
单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用在同一个作用域中,已存在同名的变量,则之后的声明初始化,则退化为赋值操作。但这个前提是,最少要有一个新的变量被定义,且在同一作用域,例如,下面的y就是新定义的变量

package main
import (
    "fmt"
)
func main() {
    x := 140
    fmt.Println(&x)
    x, y := 200, "abc"
    fmt.Println(&x, x)
    fmt.Print(y)
}
匿名变量

在使用多重赋值时候,如果不需要在左值中接收变量,可以使用匿名变量 _, python中也经常有这种用法

for data, _ in range list {
}

//python 中
data = [2,4,1,6]
for _, item in enumerate(data):
    print(item)

2. 常量的定义

常量

常量是一个简单值的标识符,在程序运行时,不会被修改的量。

显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
package main

import "fmt"

func main() {
   const LENGTH int = 10
   const WIDTH int = 5   
   var area int
   const a, b, c = 1, false, "str" //多重赋值

   area = LENGTH * WIDTH
   fmt.Printf("面积为 : %d", area)
   fmt.Println(a, b, c)   
}

常量可以作为枚举,常量组

const (
    Unknown = 0
    Female = 1
    Male = 2
)

常量组中如不指定类型和初始化值,则与上一行非空常量右值相同

package main

import (
	"fmt"
)

func main() {
	const (
		x uint16 = 16
		y
		s = "abc"
		z
	)
	fmt.Printf("%T,%v\n", y, y)
	fmt.Printf("%T,%v\n", z, z)
}

常量的注意事项:

  • 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
  • 不曾使用的常量,在编译的时候,是不会报错的
  • 显示指定类型的时候,必须确保常量左右值类型一致,需要时可做显示类型转换。这与变量就不一样了,变量是可以是不同的类型值

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量

iota 可以被用作枚举值:

const (
    a = iota
    b = iota
    c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

const (
    a = iota
    b
    c
)
package main

import "fmt"

func main() {
    const (
            a = iota   //0
            b          //1
            c          //2
            d = "ha"   //独立值,iota += 1
            e          //"ha"   iota += 1
            f = 100    //iota +=1
            g          //100  iota +=1
            h = iota   //7,恢复计数
            i          //8
    )
    fmt.Println(a,b,c,d,e,f,g,h,i)
}

如果中断iota自增,则必须显式恢复。且后续自增值按行序递增

自增默认是int类型,可以自行进行显示指定类型

数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址

使用iota能简化定义,在定义枚举时很有用。

每次 const 出现时,都会让 iota 初始化为0.

const a = iota // a=0
const (
  b = iota     //b=0
  c            //c=1   相当于c=iota
)

3. 匿名变量是什么

1. python中如何定义匿名变量?

my_list = ["test1", "test2", "test3"]
#不打印 index
for index, item in enumerate(my_list):
    print(item)

上面的第5行并没有打印index,实际上有些情况下,我们并不会使用index这个变量。但是这个时候index的变量不使用但是index这个名称却被占用了,所以这个时候我们就可以使用匿名变量。将上面的代码改成:

# 定义匿名变量
my_list = ["test1",  "test2",  "test3"]
#不打印 index
for _, item in enumerate(my_list):
    print(item)

将上面的index改为下划线 "_",表示这个地方的变量是一个匿名变量。这样既不用占用变量名又可以多一个占位符。

注意:python中申明了变量名后续代码中不使用并没有问题,但是go语言中声明了变量不使用就会报错,所以匿名变量在go语言中会更加的常用。

2. go语言中定义匿名变量

func test() (int, error)  {
	return 0, nil
}

func main() {
	_, err := test()
	if err != nil {
		fmt.Println("函数调用成功")
	}
}

说明: 第6行代码中接收函数返回值的时候使用到了匿名变量。因为此处我们并不打印返回的值而只是关心函数调用是否成功

GO基本数据类型、运算符和表达式

1. go基本数据类型

bool类型
布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true

数值型

  1. 整数型
    可以简单讲解一下二进制和位数的关系,以及int和uint的关系
    ●int8 有符号 8 位整型 (-128 到 127) 长度:8bit
    ●int16 有符号 16 位整型 (-32768 到 32767)
    ●int32 有符号 32 位整型 (-2147483648 到 2147483647)
    ●int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)
    ●uint8 无符号 8 位整型 (0 到 255) 8位都用于表示数值:
    ●uint16 无符号 16 位整型 (0 到 65535)
    ●uint32 无符号 32 位整型 (0 到 4294967295)
    ●uint64 无符号 64 位整型 (0 到 18446744073709551615)
  2. 浮点型
    ●float32 32位浮点型数
    ●float64 64位浮点型数
  3. 其他
    ●byte 等于 uint8
    ●rune 等于 int32
    ●uint 32 或 64 位

字符

Golang中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存。
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。也就是说对于传统的字符串是由字符组成的,而Go的字符串不同,它是由字节组成的。

package main

import (
	"fmt"
)

func main() {

	var a byte
	a = 'a'
	//输出ascii对应码值 。。 这里说明一下什么是ascii码
	fmt.Println(a)
	fmt.Printf("a=%c", a)
}

字符常量只能使用单引号括起来,例如:var a byte = 'a' var a int = 'a'

package main

import (
	"fmt"
)

func main() {

	var a byte
	a = "a"
	//输出ascii对应码值 。。 这里说明一下什么是ascii码
	fmt.Println(a)
	fmt.Printf("a=%c", a)
}

字符本质是一个数字, 可以进行加减乘除

package main

import (
	"fmt"
	"reflect"
)

func main() {

	a := 'a'

	//这里注意一下 1. a+1可以和数字计算 2.a+1的类型是32 3. int类型可以直接变成字符

	fmt.Println(reflect.TypeOf(a+1))
	fmt.Printf("a+1=%c", a+1)
}

字符串

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

3.运算符和表达式

1. 算数运算符

+ - * / % (求余) ++--

2. 关系运算符

== != > < >= <=

3. 逻辑运算符

&&所谓逻辑与运算符。如果两个操作数都非零,则条件变为真
****所谓的逻辑或操作。如果任何两个操作数是非零,则条件变为真
!所谓逻辑非运算符。使用反转操作数的逻辑状态。如果条件为真,那么逻辑非操后结果为假

这个和python不一样,python中使用 and or来连接

package main

import "fmt"

func main() {
   var a bool = true
   var b bool = false
   if ( a && b ) {
      fmt.Printf("第一行 - 条件为 true\n" )
   }
   if ( a || b ) {
      fmt.Printf("第二行 - 条件为 true\n" )
   }
   /* 修改 a 和 b 的值 */
   a = false
   b = true
   if ( a && b ) {
      fmt.Printf("第三行 - 条件为 true\n" )
   } else {
      fmt.Printf("第三行 - 条件为 false\n" )
   }
   if ( !(a && b) ) {
      fmt.Printf("第四行 - 条件为 true\n" )
   }
}

4. 位运算符

位运算符对整数在内存中的二进制位进行操作。

下表列出了位运算符 &, |, 和 ^ 的计算:

pqp & qp | qp ^ q
00000
01011
11110
10011

字符串的基本操作

1. 转义符

转义字符意义ASCII码值(十进制)
\n换行(LF) ,将当前位置移到下一行开头010
\r回车(CR) ,将当前位置移到本行开头013
\t水平制表(HT) (跳到下一个TAB位置)009
\代表一个反斜线字符'''092
'代表一个单引号(撇号)字符039
"代表一个双引号字符034
?代表一个问号063

2. 输入输出格式化

缺省格式和类型

格式化后的效果动词描述
[0 1]%v缺省格式
[]int64{0, 1}%#vgo语法打印
[]int64%T类型打印

整型(缩进, 进制类型, 正负符号)

格式化后的效果动词描述
15%d十进制
+15%+d必须显示正负符号
␣␣15%4dPad空格(宽度为4,右对齐)
15␣␣%-4dPad空格 (宽度为4,左对齐)
1111%b二进制
17%o八进制
f%x16进制,小写

字符(有引号, Unicode)

Value: 65 (Unicode letter A)

格式化后的效果动词描述
A%c字符
'A'%q有引号的字符
U+0041%UUnicode
U+0041 'A'%#UUnicode 有引号

浮点(缩进, 精度, 科学计数)

Value: 1234.567

格式化后的效果动词描述
1.234560e+02%e科学计数
123.456000%f十进制小数

字符串or 字节slice (引号, 缩进, 16进制)

Value: "cafe"

格式化后的效果动词描述
cafe%s字符串原样输出
␣␣cafe%6s宽度为6,右对齐

条件语句和循环语句

1. if语句

go语言的常用控制流程有if和for,没有while, 而switch和goto是为了简化代码,降低重复代码,属于扩展的流程控制。

if语句的结构:

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
}

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
} else {
  /* 在布尔表达式为 false 时执行 */
}

if 布尔表达式1 {
   /* 在布尔表达式1为 true 时执行 */
} else if 布尔表达式2{
   /* 在布尔表达式1为 false ,布尔表达式2为true时执行 */
} else{
   /* 在上面两个布尔表达式都为false时,执行*/
}
package main

import "fmt"

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

if还有一个变体也很常用

package main

import (  
    "fmt"
)

func main() {  
    if num := 10; num % 2 == 0 { //checks if number is even
        fmt.Println(num,"is even") 
    }  else {
        fmt.Println(num,"is odd")
    }
}
if err := Connect(); err != nil {
    
}

这种写法可以将返回值与判断放在一行进行处理,而且返回值的作用范围被限制在if、 else 语句组合中。

提示:

在编程中,变量在其实现了变量的功能后,作用范围越小,所造成的问题可能性越小,每一个变量代表一个状态,有状态的地方,状态就会被修改,函数的局部变量只会影响一个函数的执行, 但全局变量可能会影响所有代码的执行状态,因此限制变量的作用范围对代码的稳定性有很大的帮助 。

package main

import (
	"errors"
	"fmt"
)

func test() error {
	return errors.New("error")
}

func main() {
	if err := test(); err != nil {
		fmt.Println("error happen")
	}
	fmt.Println(err) //此处会报错
}

2. for循环

  1. Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。
//和 C 语言的 for 一样:
for init; condition; post { }
//和 C 的 while 一样:
for condition { }
//和 C 的 for(;;) 一样:
for { }
  • init: 一般为赋值表达式,给控制变量赋初值;
  • condition: 关系表达式或逻辑表达式,循环控制条件;
  • post: 一般为赋值表达式,给控制变量增量或减量。

for语句执行过程如下:

  • 1、先对表达式 1 赋初值;
  • 2、判别赋值表达式 init 是否满足给定条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。

  1. for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
    newMap[key] = value
}
package main
import "fmt"

func main() {
        strings := []string{"bobby", "imooc"}
        for i, s := range strings {
                fmt.Println(i, s)
        }


        numbers := [6]int{1, 2, 3, 5}
        for i,x:= range numbers {
                fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
        }  
}

3. goto语句

Go 语言的 goto 语句可以无条件地转移到过程中指定的行。

goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。

但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。

goto 语法格式如下:

package main

import "fmt"

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

   /* 循环 */
   LOOP: for a < 20 {
      if a == 15 {
         /* 跳过迭代 */
         a = a + 1
         goto LOOP
      }
      fmt.Printf("a的值为 : %d\n", a)
      a++    
   }  
}

应用场景

使用 goto 退出多层循环
package main
    import "fmt"
    func main() {
        var breakAgain bool
        // 外循环
        for x := 0; x < 10; x++ {
            // 内循环
            for y := 0; y < 10; y++ {
                // 满足某个条件时, 退出循环
                if y == 2 {
                    // 设置退出标记
                    breakAgain = true
                    // 退出本次循环
                    break
                }
            }
            // 根据标记, 还需要退出一次循环
            if breakAgain {
                    break
            }
        }
        fmt.Println("done")
    }

优化后

package main
    import "fmt"
    func main() {
        for x := 0; x < 10; x++ {
            for y := 0; y < 10; y++ {
                if y == 2 {
                    // 跳转到标签
                    goto breakHere
                }
            }
        }
        // 手动返回, 避免执行进入标签
        return
        // 标签
    breakHere:
        fmt.Println("done")
    }
使用 goto 集中处理错误

多处错误处理存在代码重复时是非常棘手的,例如:

err := firstCheckError()
    if err != nil {
        fmt.Println(err)
        exitProcess()
        return
    }
    err = secondCheckError()
    if err != nil {
        fmt.Println(err)
        exitProcess()
        return
    }
    fmt.Println("done")

上述代码修改一下

err := firstCheckError()
        if err != nil {
            goto onExit
        }
        err = secondCheckError()
        if err != nil {
            goto onExit
        }
        fmt.Println("done")
        return
    onExit:
        fmt.Println(err)
        exitProcess()

4. switch语句

  1. switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。
switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}
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("优秀!\n" )    
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" );
   }
   fmt.Printf("你的等级是 %s\n", grade );      
}

Type Switch

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

Type Switch 语法格式如下:

switch x.(type){
    case type:
       statement(s);      
    case type:
       statement(s); 
    /* 你可以定义任意个数的case */
    default: /* 可选 */
       statement(s);
}
package main

import "fmt"

func main() {
   var x interface{}
     
   switch i := x.(type) {
      case nil:  
         fmt.Printf(" x 的类型 :%T",i)                
      case int:  
         fmt.Printf("x 是 int 型")                      
      case float64:
         fmt.Printf("x 是 float64 型")          
      case func(int) float64:
         fmt.Printf("x 是 func(int) 型")                      
      case bool, string:
         fmt.Printf("x 是 bool 或 string 型" )      
      default:
         fmt.Printf("未知型")    
   }  
}

1.分支多值

不同的 case 表达式使用逗号分隔。

var a = "mum"
switch a {
    case "mum", "daddy":
    fmt.Println("family")
}

2.分支表达式

var r int = 11
switch {
    case r > 10 && r < 20:
    fmt.Println(r)
}

5.python中为什么没有switch

查看Python官方:PEP 3103-A Switch/Case Statement
发现其实实现Switch Case需要被判断的变量是可哈希的和可比较的,这与Python倡导的灵活性有冲突。在实现上,优化不好做,可能到最后最差的情况汇编出来跟If Else组是一样的。所以Python没有支持。

https://www.python.org/dev/peps/pep-3103/
    
score = 90
switch = {
    90: lambda : print("A"),
    80: lambda : print("B"),
    70: lambda : print("C"),
}

switch[score]()
class switch(object):
    def __init__(self, value):
        self.value = value
        self.fall = False

    def __iter__(self):
        """Return the match method once, then stop"""
        yield self.match
        raise StopIteration

    def match(self, *args):
        """Indicate whether or not to enter a case suite"""
        if self.fall or not args:
            return True
        elif self.value in args: # changed for v1.5, see below
            self.fall = True
            return True
        else:
            return False

# The following example is pretty much the exact use-case of a dictionary,
# but is included for its simplicity. Note that you can include statements
# in each suite.
v = 'ten'
for case in switch(v):
    if case('one'):
        print(1)
        break
    if case('two'):
        print(2)
        break
    if case('ten'):
        print(10)
        break
    if case('eleven'):
        print(11)
        break
    if case(): # default, could also just omit condition or 'if True'
        print("something else!")
        # No need to break here, it'll stop anyway

# break is used here to look as much like the real thing as possible, but
# elif is generally just as good and more concise.

# Empty suites are considered syntax errors, so intentional fall-throughs
# should contain 'pass'
c = 'z'
for case in switch(c):
    if case('a'): pass # only necessary if the rest of the suite is empty
    if case('b'): pass
    # ...
    if case('y'): pass
    if case('z'):
        print("c is lowercase!")
        break
    if case('A'): pass
    # ...
    if case('Z'):
        print("c is uppercase!")
        break
    if case(): # default
        print("I dunno what c was!")

# As suggested by Pierre Quentel, you can even expand upon the
# functionality of the classic 'case' statement by matching multiple
# cases in a single shot. This greatly benefits operations such as the
# uppercase/lowercase example above:
import string
c = 'A'
for case in switch(c):
    if case(*string.lowercase): # note the * for unpacking as arguments
        print("c is lowercase!")
        break
    if case(*string.uppercase):
        print("c is uppercase!")
        break
    if case('!', '?', '.'): # normal argument passing style also applies
        print("c is a sentence terminator!")
        break
    if case(): # default
        print("I dunno what c was!")

# Since Pierre's suggestion is backward-compatible with the original recipe,
# I have made the necessary modification to allow for the above usage.

最常用的复杂数据类型-map、数组、列表和切片

1. python的数组细节

对于Python列表-我们可以由此得出结论,对于每个新元素,我们需要另外八个字节来引用新对象。新的整数对象本身消耗28个字节。列表“ lst”的大小,不包含元素的大小,可以使用以下公式计算:

64 + 8 * len(lst) + + len(lst) * 28

image.png

python中的list是python的内置数据类型,list中的数据类不必相同的,而array的中的类型必须全部相同。在list中的数据类型保存的是数据的存放的地址,简单的说就是指针,并非数据,这样保存一个list就太麻烦了,例如list1=[1,2,3,'a']需要4个指针和四个数据,增加了存储和消耗cpu。

array底层是C语言。有步长strides,dimensions,data三属性。固定类型的numpy类型的数组array缺乏这种灵活,但是更方便进行存储和处理数据。

96 + n * 8 Bytes

image.png

数组还有一个特性:数组的大小一开始需要指定好,后期动态扩充会拷贝值

对于机器学习的同学来说:数组用的更多,一般都是用numpy的array。算法上不会使用list

from array import array

#arr的类型 https://docs.python.org/3/library/array.html
a = array('b', [1, 3, 5])
for item in a:
    print(item)

#使用切片获取
numbers_list = [2, 5, 62, 5, 42, 52, 48, 5]
numbers_array = array('i', numbers_list)

print(numbers_array[2:5]) # 3rd to 5th
print(numbers_array[:-5]) # beginning to 4th
print(numbers_array[5:])  # 6th to end
print(numbers_array[:])   # beginning to end

#添加元素
numbers = array('i', [1, 2, 3, 5, 7, 10])

# changing first element
numbers[0] = 0
print(numbers)     # Output: array('i', [0, 2, 3, 5, 7, 10])

# changing 3rd to 5th element
numbers[2:5] = array('i', [4, 6, 8])
print(numbers)     # Output: array('i', [0, 2, 4, 6, 8, 10])

#通过extend和append方法扩展数组
numbers = array('i', [1, 2, 3])

numbers.append(4)
print(numbers)     # Output: array('i', [1, 2, 3, 4])

# extend() appends iterable to the end of the array
numbers.extend([5, 6, 7])
print(numbers)     # Output: array('i', [1, 2, 3, 4, 5, 6, 7])

#使用+连接数组
odd = array('i', [1, 3, 5])
even = array('i', [2, 4, 6])

numbers = array('i')   # creating empty array of integer
numbers = odd + even
print(numbers)

2. go语言的数组

机器学习的同学对python的array应该很了解,但是做web开发的同学对python的array使用的不多

Go 语言提供了数组类型的数据结构。 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。

数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从0开始,到长度减1。

数组一旦定义后,大小不能更改。

数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。

相对于去声明 number0, number1, ..., number99 的变量,使用数组形式 numbers[0], numbers[1] ..., numbers[99] 更加方便且易于扩展。

数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。
图片.png

声明和初始化数组

需要指明数组的大小和存储的数据类型。

var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:

var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}

balance[4] = 50.0

数组的其他创建方式:

var a [4] float32 // 等价于:var arr2 = [4]float32{}
fmt.Println(a) // [0 0 0 0]
var b = [5] string{"ruby", "王二狗", "rose"}
fmt.Println(b) // [ruby 王二狗 rose  ]
var c = [5] int{'A', 'B', 'C', 'D', 'E'} // byte
fmt.Println(c) // [65 66 67 68 69]
d := [...] int{1,2,3,4,5}// 根据元素的个数,设置数组的大小
fmt.Println(d)//[1 2 3 4 5]
e := [5] int{4: 100} // [0 0 0 0 100]
fmt.Println(e)
f := [...] int{0: 1, 4: 1, 9: 1} // [1 0 0 0 1 0 0 0 0 1]
fmt.Println(f)

访问数组元素

package main

import "fmt"

func main() {
   var n [10]int /* n 是一个长度为 10 的数组 */
   var i,j int

   /* 为数组 n 初始化元素 */         
   for i = 0; i < 10; i++ {
      n[i] = i + 100 /* 设置元素为 i + 100 */
   }

   /* 输出每个数组元素的值 */
   for j = 0; j < 10; j++ {
      fmt.Printf("Element[%d] = %d\n", j, n[j] )
   }
}

数组的长度

通过将数组作为参数传递给len函数,可以获得数组的长度。

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    fmt.Println("length of a is",len(a))

}

您甚至可以忽略声明中数组的长度并将其替换为…让编译器为你找到长度。这是在下面的程序中完成的。

package main

import (  
    "fmt"
)

func main() {  
    a := [...]int{12, 78, 50} // ... makes the compiler determine the length
    fmt.Println(a)
}

遍历数组:

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    for i := 0; i < len(a); i++ { //looping from 0 to the length of the array
        fmt.Printf("%d th element of a is %.2f\n", i, a[i])
    }
}

使用range遍历数组:

package main

import "fmt"

func main() {  
    a := [...]float64{67.7, 89.8, 21, 78}
    sum := float64(0)
    for i, v := range a {//range returns both the index and value
        fmt.Printf("%d the element of a is %.2f\n", i, v)
        sum += v
    }
    fmt.Println("\nsum of all elements of a",sum)
}

如果您只需要值并希望忽略索引,那么可以通过使用_ blank标识符替换索引来实现这一点。

for _, v := range a { //ignores index  
}

数组是值类型

数组是值类型 Go中的数组是值类型,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。

package main

import "fmt"

func main() {  
    a := [...]string{"USA", "China", "India", "Germany", "France"}
    b := a // a copy of a is assigned to b
    b[0] = "Singapore"
    fmt.Println("a is ", a)
    fmt.Println("b is ", b) 
}

数组的大小是类型的一部分。因此[5]int和[25]int是不同的类型。因此,数组不能被调整大小。不要担心这个限制,因为切片的存在是为了解决这个问题。

package main

func main() {  
    a := [3]int{5, 78, 8}
    var b [5]int
    b = a //not possible since [3]int and [5]int are distinct types
}
var al []int     //创建slice
//sl := make([]int,10)  //创建有10个元素的slice
sl:=[3]int{1,2,3} //创建有初始化元素的slice
s2:=[4]int{1,2,3,4} //创建有初始化元素的slice
fmt.Printf("%T, %T, %T", al, sl, s2)

3. go语言的切片

1. 什么是切片

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大

Go的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。 本文将深入切片的本质,并讲解它的用法。

2. 定义切片

// 第一种
var identifier []type
//第二种 使用make
var slice1 []type = make([]type, len)
简写成
slice1 := make([]type, len)
#使用make来创建slice,map,chanel说明如下

#第三种,通过对数组操作返回
course := [5]string{"django", "tornado", "scrapy", "python", "asyncio"}
subCourse := course[1:2]
fmt.Printf("%T", subCourse)

3. 切片初始化

s :=[] int {1,2,3 } 
直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := arr[:]

创建 slice 的方式有以下几种:

序号方式代码示例
1直接声明var slice []int
2newslice := *new([]int)
3字面量slice := []int{1,2,3,4,5}
4makeslice := make([]int, 5, 10)
5从切片或数组“截取”slice := array[1:5]** 或 **slice := sourceSlice[1:5]

4. 用法

5. 底层存储

Go slice - 用法和内部原理

6. go和python的切片区别

记go和python中的slice一个简单区别

7. slice扩容机制

append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。

append 函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。

使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。

这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍 原 slice 容量超过 1024,新 slice 容量大家可以通过源码了解, 文章推荐:

深度解密Go语言之Slice

4. 切片原理

图片.png

5. go语言的map

map创建

要求所有的key的数据类型相同,所有value数据类型相同(注:key与value可以有不同的数据类型)

// 1 字面值
{
    m1 := map[string]string{
        "m1": "v1", // 定义时指定的初始key/value, 后面可以继续添加
    }
    _ = m1

}

// 2 使用make函数
{
    m2 := make(map[string]string) // 创建时,里面不含元素,元素都需要后续添加
    m2["m2"] = "v2"               // 添加元素
    _ = m2

}

// 定义一个空的map
{
    m3 := map[string]string{}
    m4 := make(map[string]string)
    _ = m3
    _ = m4
}

map中key的类型

map中的每个key在keys的集合中是唯一的,而且需要支持 == or != 操作

key的常用类型:int, rune, string, 结构体(每个元素需要支持 == or != 操作), 指针, 基于这些类型自定义的类型

// m0 可以, key类型为string, 支持 == 比较操作
{
    var m0 map[string]string // 定义map类型变量m0,key的类型为string,value的类型string
    fmt.Println(m0)
}

// m1 不可以, []byte是slice,不支持 == != 操作,不可以作为map key的数据类型
{
    //var m1 map[[]byte]string // 报错: invalid map key type []byte
    //fmt.Println(m1)

    // 准确说slice类型只能与nil比较,其他的都不可以,可以通过如下测试:
    // var b1,b2 []byte
    // fmt.Println(b1==b2) // 报错: invalid operation: b1 == b2 (slice can only be compared to nil)
}

// m2 可以, interface{}类型可以作为key,但是需要加入的key的类型是可以比较的
{
    var m2 map[interface{}]string
    m2 = make(map[interface{}]string)
    //m2[[]byte("k2")]="v2" // panic: runtime error: hash of unhashable type []uint8
    m2[123] = "123"
    m2[12.3] = "123"
    fmt.Println(m2)
}

// m3 可以, 数组支持比较
{
    a3 := [3]int{1, 2, 3}
    var m3 map[[3]int]string
    m3 = make(map[[3]int]string)
    m3[a3] = "m3"
    fmt.Println(m3)
}

// m4 可以,book1里面的元素都是支持== !=
{
    type book1 struct {
        name string
    }
    var m4 map[book1]string
    fmt.Println(m4)
}

// m5 不可以, text元素类型为[]byte, 不满足key的要求
{
    // type book2 struct {
    // 	name string
    // 	text []byte //没有这个就可以
    // }
    //var m5 map[book2]string //invalid map key type book2
    //fmt.Println(m5)
}

map的增删改

// 创建
m := map[string]string{
    "a": "va",
    "b": "vb",
}
fmt.Println(len(m)) // len(m) 获得m中key/value对的个数

// 增加,修改
{
    // k不存在为增加,k存在为修改
    m["c"] = ""
    m["c"] = "11"                      // 重复增加(key相同),使用新的值覆盖
    fmt.Printf("%#v %#v\n", m, len(m)) // map[string]string{"a":"va", "b":"vb", "c":"11"} 3
}

// 查
{
    // v := m[k] // 从m中取键k对应的值给v,如果k在m中不存在,则将value类型的零值赋值给v
    // v, ok := m[k] // 从m中取键k对应的值给v,如果k存在,ok=true,如果k不存在,将value类型的零值赋值给v同时ok=false
    // 查1 - 元素不存在
    v1 := m["x"] //
    v2, ok2 := m["x"]
    fmt.Printf("%#v %#v %#v\n", v1, v2, ok2) // "" "" false

    // 查2 - 元素存在
    v3 := m["a"]
    v4, ok4 := m["a"]
    fmt.Printf("%#v %#v %#v\n", v3, v4, ok4) //"va" "va" true
}

// 删, 使用内置函数删除k/v对
{
    // delete(m, k) 将k以及k对应的v从m中删掉;如果k不在m中,不执行任何操作
    delete(m, "x")                     // 删除不存在的key,原m不影响
    delete(m, "a")                     // 删除存在的key
    fmt.Printf("%#v %#v\n", m, len(m)) // map[string]string{"b":"vb", "c":"11"} 2
    delete(m, "a")                     // 重复删除不报错,m无影响
    fmt.Printf("%#v %#v\n", m, len(m)) /// map[string]string{"b":"vb", "c":"11"} 2
}

map的遍历

●遍历的顺序是随机的
●使用for range遍历的时候,k,v使用的同一块内存,这也是容易出现错误的地方

m := map[string]int{
		"a": 1,
		"b": 2,
	}
	for k, v := range m {
		fmt.Printf("k:[%v].v:[%v]\n", k, v) // 输出k,v值
	}
map遍历易错点举例

由于遍历的时候,遍历v使用的同一块地址,同时这块地址是临时分配的。虽然v的地址没有变化,但v的内容在一直变化,当遍历完成后,v的内容是map遍历时最后遍历的元素的值(map遍历无序,每次不确定哪个元素是最后一个元素)。当程序将v的地址放入到slice中的时候,slice在不断地v的地址插入,由于v一直是那块地址,因此slice中的每个元素记录的都是v的地址。因此当打印slice中的内容的时候,都是同一个值

m := map[string]int{
    "a": 1,
    "b": 2,
}
var bs []*int
for k, v := range m {
    fmt.Printf("k:[%p].v:[%p]\n", &k, &v) // 这里的输出可以看到,k一直使用同一块内存,v也是这个状况
    bs = append(bs, &v) // 对v取了地址
}

// 输出
for _, b := range bs {
    fmt.Println(*b) // 输出都是1或者都是2
}

Go语言如何解决面向对象问题

1. 结构体定义和使用细节

结构体类型的定义

type Course struct {
  price int
  name string
  url string
}

Course结构体内部有三个变量,分别是价格、课程名、url。特别需要注意是结构体内部变量的大小写,首字母大写是公开变量,首字母小写是内部变量,分别相当于类成员变量的 Public 和 Private 类别。内部变量只有属于同一个 package(简单理解就是同一个目录)的代码才能直接访问。

结构体变量的创建

创建一个结构体变量有多种形式,我们先看结构体变量最常见的创建形式

package main

import "fmt"

type Course struct {
  price int
  name string
  url string
}

func main() {
    var c Course = Course {
        price: 100,
        name: "scrapy分布式爬虫",
        url: "",  // 注意这里的逗号不能少
    }
    fmt.Printf("%+v\n", c)
}

通过显示指定结构体内部字段的名称和初始值来初始化结构体,可以只指定部分字段的初值,甚至可以一个字段都不指定,那些没有指定初值的字段会自动初始化为相应类型的「零值」。这种形式我们称之为 「KV 形式」。

结构体的第二种创建形式是不指定字段名称来顺序字段初始化,需要显示提供所有字段的初值,一个都不能少。这种形式称之为「顺序形式」。

package main

import "fmt"

type Course struct {
  price int
  name string
  url string
}

func main() {
    var c Course = Course {100, "scrapy分布式爬虫", ""}
    fmt.Printf("%+v\n", c)
}

结构体变量和普通变量都有指针形式,使用取地址符就可以得到结构体的指针类型

var c *Course = &Course {100, "scrapy分布式爬虫", ""}

使用new() 函数来创建一个「零值」结构体

var c *Course = new(Course)

注意 new() 函数返回的是指针类型。下面再引入结构体变量的第四种创建形式,这种形式也是零值初始化,就数它看起来最不雅观。

var c Course

最后我们再将三种零值初始化形式放到一起对比观察一下

var c1 Course = Course{}
var c2 Course
var c3 *Course = new(Course)

零值结构体和 nil 结构体

nil 结构体是指结构体指针变量没有指向一个实际存在的内存。这样的指针变量只会占用 1 个指针的存储空间,也就是一个机器字的内存大小。

var c *Course = nil

而零值结构体是会实实在在占用内存空间的,只不过每个字段都是零值。如果结构体里面字段非常多,那么这个内存空间占用肯定也会很大。

结构体的拷贝

结构体之间可以相互赋值,它在本质上是一次浅拷贝操作,拷贝了结构体内部的所有字段。结构体指针之间也可以相互赋值,它在本质上也是一次浅拷贝操作,不过它拷贝的仅仅是指针地址值,结构体的内容是共享的。

package main

import "fmt"

type Course struct {
  price int
  name string
  url string
}


func main() {
    var c1 Course = Course {50, "scrapy分布式爬虫", ""}
    var c2 Course = c1
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)
    c1.price = 100
    fmt.Printf("%+v\n", c1)
    fmt.Printf("%+v\n", c2)

    var c3 *Course = &Course{50, "scrapy分布式爬虫", ""}
    var c4 *Course = c3
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
    c3.price = 100
    fmt.Printf("%+v\n", c3)
    fmt.Printf("%+v\n", c4)
}

slice的结构体

通过观察 Go 语言的底层源码,可以发现所有的 Go 语言内置的高级数据结构都是由结构体来完成的。
切片头的结构体形式如下,它在 64 位机器上将会占用 24 个字节

type slice struct {
array unsafe.Pointer  // 底层数组的地址
len int // 长度
cap int // 容量
}

此处解释一下slice的函数传递本质上也是值传递

字符串头的结构体

它在 64 位机器上将会占用 16 个字节

type string struct {
  array unsafe.Pointer // 底层数组的地址
  len int
}

map的结构体

type hmap struct {
  count int
  ...
  buckets unsafe.Pointer  // hash桶地址
  ...
}

解释一下下面的情况

在数组与切片章节,我们自习分析了数组与切片在内存形式上的区别。数组只有「体」,切片除了「体」之外,还有「头」部。切片的头部和内容体是分离的,使用指针关联起来。请读者尝试解释一下下面代码的输出结果

package main

import "fmt"
import "unsafe"

type ArrayStruct struct {
    value [10]int
}

type SliceStruct struct {
    value []int
}

func main() {
    var as = ArrayStruct{[...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
    var ss = SliceStruct{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}}
    fmt.Println(unsafe.Sizeof(as), unsafe.Sizeof(ss))
}

结构体的参数传递

结构体是值传递

package main

import "fmt"

type Course struct {
	price int
	name string
	url string
}

func changeCourse(c Course){
	c.price = 200
}

func main() {
	var c Course = Course {
		price: 100,
		name: "scrapy分布式爬虫",
		url: "",  // 注意这里的逗号不能少
	}
	changeCourse(c)
	fmt.Println(c.price)
}

2. 结构体也有继承?

结构体作为一种变量它可以放进另外一个结构体作为一个字段来使用,这种内嵌结构体的形式在 Go 语言里称之为「组合」。下面我们来看看内嵌结构体的基本使用方法

package main

import "fmt"

type Teacher struct {
	name string
	age int
	title string
}


type Course struct {
	teacher Teacher
	price int
	name string
	url string
}

func getInfo(c Course){
	fmt.Println(c.teacher.name, c.teacher.age)
}

func main() {
	var c Course = Course {
		teacher: Teacher{
			name:"bobby",
			age:18,
			title: "架构师",
		},
		price: 100,
		name: "scrapy分布式爬虫",
		url: "",  // 注意这里的逗号不能少
	}
	getInfo(c)
}

匿名内嵌结构体

还有一种特殊的内嵌结构体形式,内嵌的结构体不提供名称。这时外面的结构体将直接继承内嵌结构体所有的内部字段和方法,就好像把子结构体的一切全部都揉进了父结构体一样。匿名的结构体字段将会自动获得以结构体类型的名字命名的字段名称

package main

import "fmt"

type Teacher struct {
	name string
	age int
	title string
}


type Course struct {
	Teacher
	price int
	name string
	url string
}

func getInfo(c Course){
	fmt.Println(c.name, c.age)
}

func main() {
	var c Course = Course {
		Teacher: Teacher{ //还可以这样声明一些属性值,因为Teacher是结构体,匿名,所以需要这样声明
			"bobby", 18, "",
		},
		price: 100,
		name: "scrapy分布式爬虫",
		url: "",  // 注意这里的逗号不能少
	}
	getInfo(c)
}

如果嵌入结构的字段和外部结构的字段相同,那么,想要修改嵌入结构的字段值需要加上外部结构中声明的嵌入结构名称

func getInfo(c Course){
	fmt.Println(c.Teacher.name, c.age)
}

Go 语言的结构体没有多态性

Go 语言不是面向对象语言在于它的结构体不支持多态,它不能算是一个严格的面向对象语言。多态是指父类定义的方法可以调用子类实现的方法,不同的子类有不同的实现,从而给父类的方法带来了多样的不同行为。但是go语言支持鸭子类型

所谓的继承仅仅是形式上的语法糖,c.show() 被转换成二进制代码后和 c.Point.show() 是等价的,c.x 和 c.Point.x 也是等价的。

3. 结构体标签

结构体的字段除了名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。比如在我们解析json或生成json文件时,常用到encoding/json包,它提供一些默认标签,例如:omitempty标签可以在序列化的时候忽略0值或者空值。而-标签的作用是不进行序列化,其效果和和直接将结构体中的字段写成小写的效果一样。

type Info struct {
    Name string
    Age  int `json:"age,omitempty"`
    Sex  string
}

在序列化和反序列化的时候,也支持类型转化等操作。如

type Info struct {
    Name string
    Age  int   `json:"age,string"`
    //这样生成的json对象中,age就为字符串
    Sex  string
}

现在来了解下如何设置自定义的标签,以及如何像官方包一样,可以通过标签,对字段进行自定义处理。要实现这些,我们要用到reflect包。

package main

import (
    "fmt"
    "reflect"
)

const tagName = "Testing"

type Info struct {
    Name string `Testing:"-"`
    Age  int    `Testing:"age,min=17,max=60"`
    Sex  string `Testing:"sex,required"`
}

func main() {
    info := Info{
        Name: "benben",
        Age:  23,
        Sex:  "male",
    }

    //通过反射,我们获取变量的动态类型
    t := reflect.TypeOf(info)
    fmt.Println("Type:", t.Name())
    fmt.Println("Kind:", t.Kind())

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i) //获取结构体的每一个字段
        tag := field.Tag.Get(tagName)
        fmt.Printf("%d. %v (%v), tag: '%v'\n", i+1, field.Name, field.Type.Name(), tag)
    }
}

4. 结构体方法

Go 语言不是面向对象的语言,它里面不存在类的概念,结构体正是类的替代品。类可以附加很多成员方法,结构体也可以。

package main

import "fmt"
import "math"

type Circle struct {
 x int
 y int
 Radius int
}

// 面积
func (c Circle) Area() float64 {
 return math.Pi * float64(c.Radius) * float64(c.Radius)
}

// 周长
func (c Circle) Circumference() float64 {
 return 2 * math.Pi * float64(c.Radius)
}

func main() {
 var c = Circle {Radius: 50}
 fmt.Println(c.Area(), c.Circumference())
 // 指针变量调用方法形式上是一样的
 var pc = &c
 fmt.Println(pc.Area(), pc.Circumference())
}

Go 语言不喜欢类型的隐式转换,所以需要将整形显示转换成浮点型,不是很好看,不过这就是 Go 语言的基本规则,显式的代码可能不够简洁,但是易于理解。

Go 语言的结构体方法里面没有 self 和 this 这样的关键字来指代当前的对象,它是用户自己定义的变量名称,通常我们都使用单个字母来表示。

Go 语言的方法名称也分首字母大小写,它的权限规则和字段一样,首字母大写就是公开方法,首字母小写就是内部方法,只能归属于同一个包的代码才可以访问内部方法。

结构体的值类型和指针类型访问内部字段和方法在形式上是一样的。这点不同于 C++ 语言,在 C++ 语言里,值访问使用句点 . 操作符,而指针访问需要使用箭头 -> 操作符。

结构体指针方法

如果使用上面的方法形式给 Circle 增加一个扩大半径的方法,你会发现半径扩大不了。

func (c Circle) expand() {
  c.Radius *= 2
}

这是因为上面的方法和前面的 expandByValue 函数是等价的,只不过是把函数的第一个参数挪了位置而已,参数传递时会复制了一份结构体内容,起不到扩大半径的效果。这时候就必须要使用结构体的指针方法

func (c *Circle) expand() {
  c.Radius *= 2
}

结构体指针方法和值方法在调用时形式上是没有区别的,只不过一个可以改变结构体内部状态,而另一个不会。指针方法使用结构体值变量可以调用,值方法使用结构体指针变量也可以调用。

通过指针访问内部的字段需要 2 次内存读取操作,第一步是取得指针地址,第二部是读取地址的内容,它比值访问要慢。但是在方法调用时,指针传递可以避免结构体的拷贝操作,结构体比较大时,这种性能的差距就会比较明显。

还有一些特殊的结构体它不允许被复制,比如结构体内部包含有锁时,这时就必须使用它的指针形式来定义方法,否则会发生一些莫名其妙的问题。

5. 你可能并不懂python的鸭子类型

image.png

1. 很多人都把眼光放在了面向对象上

2. python用法简单的根本 - 鸭子类型(基于协议编程)

from collections.abc import Iterable

class Company:
    def __init__(self, employee_list):
        self.employee = employee_list

    def __iter__(self):
        return iter(self.employee)

    def __getitem__(self, item):
        return self.employee[item]


if __name__ == "__main__":
    company = Company(["tom", "bob", "jane"])
    if isinstance(company, Iterable):
        print("company是iterable类型")
    for item in company:
        print (item)

a = []
if isinstance(a, Iterable):
    print("yes")

go语言的函数

panic和recover函数

错误和异常是两个不同的概念,非常容易混淆。很多程序员习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。从表面上看,一切皆错误的思路更简单,而异常的引入仅仅增加了额外的复杂度。

但事实并非如此。众所周知,Golang遵循“少即是多”的设计哲学,追求简洁优雅,就是说如果异常价值不大,就不会将异常加入到语言特性中。

错误和异常处理是程序的重要组成部分,我们先看看下面几个问题:

  1. 错误和异常如何区分?
  2. 错误处理的方式有哪几种?
  3. 什么时候需要使用异常终止程序?
  4. 什么时候需要捕获异常?

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

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类型的变量进行赋值,以便上层函数继续走错误处理流程。

什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。

在这个启示下,我们给出异常处理的作用域(场景):

  1. 空指针引用
  2. 下标越界
  3. 除数为0
  4. 不应该出现的分支,比如default
  5. 输入不应该引起函数错误

其他场景我们使用错误处理,这使得我们的函数接口很精炼。对于异常,我们可以选择在一个合适的上游去recover,并打印堆栈信息,使得部署后的程序不会终止。

说明: Golang错误处理方式一直是很多人诟病的地方,有些人吐槽说一半的代码都是"if err != nil { / 打印 && 错误处理 / }",严重影响正常的处理逻辑。当我们区分错误和异常,根据规则设计函数,就会大大提高可读性和可维护性。

错误处理的正确姿势

1. 失败的原因只有一个时,不使用error
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,而不是error,重构一下代码:

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

说明:大多数情况,导致失败的原因不止一种,尤其是对I/O操作而言,用户需要了解更多的错误信息,这时的返回值类型不再是简单的bool,而是error。

2. 没有失败时,不使用error
3. error应放在返回值类型列表的最后
4. 错误值统一定义,而不是跟着感觉走

很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value可能为:

  1. "record is not existed."
  2. "record is not exist!"
  3. "###record is not existed!!!"
  4. ...

这使得相同的错误value撒在一大片代码里,当上层函数要对特定错误value进行统一处理时,需要漫游所有下层代码,以保证错误value统一,不幸的是有时会有漏网之鱼,而且这种方式严重阻碍了错误value的重构。

于是,我们可以参考C/C++的错误码定义文件,在Golang的每个包中增加一个错误对象定义文件,如下所示:

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

我们一般通过判断error的值来处理错误,如果当前操作失败,需要将本函数中已经create的资源destroy掉,示例代码如下:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    err = createResource2()
    if err != nil {
        destroyResource1()
        return ERR_CREATE_RESOURCE2_FAILED
    }
    err = createResource3()
    if err != nil {
        destroyResource1()
        destroyResource2()
        return ERR_CREATE_RESOURCE3_FAILED
    }
    err = createResource4()
    if err != nil {
        destroyResource1()
        destroyResource2()
        destroyResource3()
        return ERR_CREATE_RESOURCE4_FAILED
    }
    return nil
}

当Golang的代码执行时,如果遇到defer的闭包调用,则压入堆栈。当函数返回时,会按照后进先出的顺序调用闭包。

对于闭包的参数是值传递,而对于外部变量却是引用传递,所以闭包中的外部变量err的值就变成外部函数返回时最新的err值。

根据这个结论,我们重构上面的示例代码:

func deferDemo() error {
    err := createResource1()
    if err != nil {
        return ERR_CREATE_RESOURCE1_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource1()
        }
    }()
    err = createResource2()
    if err != nil {
        return ERR_CREATE_RESOURCE2_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource2()
        }
    }()
    err = createResource3()
    if err != nil {
        return ERR_CREATE_RESOURCE3_FAILED
    }
    defer func() {
        if err != nil {
            destroyResource3()
        }
    }()
    err = createResource4()
    if err != nil {
        return ERR_CREATE_RESOURCE4_FAILED
    }
    return nil
}
7. 当发生错误时,不忽略有用的返回值

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

对待异常的态度

1. 在程序开发阶段,坚持速错

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

2. 在程序部署后,应恢复异常避免程序终止

我们在调用recover的延迟函数中以最合理的方式响应该异常:
1打印堆栈的异常调用信息和关键的业务信息,以便这些问题保留可见;
2将异常转换为错误,以便调用者让程序恢复到健康状态并继续安全运行。
我们看一个简单的例子:

func funcA() error {
    defer func() {
        if p := recover(); p != nil {
            fmt.Printf("panic recover! p: %v", p)
            debug.PrintStack()
        }
    }()
    return funcB()
}
func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}
func test() {
    err := funcA()
    if err == nil {
        fmt.Printf("err is nil\\n")
    } else {
        fmt.Printf("err is %v\\n", err)
    }
}

我们期望test函数的输出是:

err is foo

实际上test函数的输出是:

err is nil

原因是panic异常处理机制不会自动将错误信息传递给error,所以要在funcA函数中进行显式的传递,代码如下所示:

func funcA() (err error) {
    defer func() {
        if p := recover(); p != nil {
            fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {
                err = errors.New(str)
            } else {
                err = errors.New("panic")
            }
            debug.PrintStack()
        }
    }()
    return funcB()
}
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设计

入参不应该有问题一般指的是硬编码,我们先看“一个启示”一节中提到的两个函数(Compile和MustCompile),其中MustCompile函数是对Compile函数的包装:

func MustCompile(str string) *Regexp {
    regexp, error := Compile(str)
    if error != nil {
        panic(`regexp: Compile(` + quote(str) + `): ` + error.Error())
    }
    return regexp
}

所以,对于同时支持用户输入场景和硬编码场景的情况,一般支持硬编码场景的函数是对支持用户输入场景函数的包装。

对于只支持硬编码单一场景的情况,函数设计时直接使用panic,即返回值类型列表中不会有error,这使得函数的调用处理非常方便(没有了乏味的"if err != nil {/ 打印 && 错误处理 /}"代码块)。


标题:GO语言基础
作者:peter7845
地址:https://blog.peterpz.top/articles/2022/07/19/1658215696620.html