var名称类型是声明单个变量的语法。
var name string
name = "a"
var name ="a"
var b = 10
c : = 10
这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值
package main
var a = "a"
var b string = "b"
var c bool
func main(){
println(a, b, c)
}
第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值
var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3
第二种,直接赋值,下面的变量类型可以是不同的类型
var name1, name2, name3 = v1, v2, v3
第三种,集合类型
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)
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
显式类型定义: 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 可以被用作枚举值:
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
)
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语言中会更加的常用。
func test() (int, error) {
return 0, nil
}
func main() {
_, err := test()
if err != nil {
fmt.Println("函数调用成功")
}
}
说明: 第6行代码中接收函数返回值的时候使用到了匿名变量。因为此处我们并不打印返回的值而只是关心函数调用是否成功
bool类型
布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
数值型
字符
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 文本。
+ - * / % (求余) ++--
== != > < >= <=
&& | 所谓逻辑与运算符。如果两个操作数都非零,则条件变为真 | ||
---|---|---|---|
** | ** | 所谓的逻辑或操作。如果任何两个操作数是非零,则条件变为真 | |
! | 所谓逻辑非运算符。使用反转操作数的逻辑状态。如果条件为真,那么逻辑非操后结果为假 |
这个和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" )
}
}
位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &, |, 和 ^ 的计算:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
转义字符 | 意义 | ASCII码值(十进制) |
---|---|---|
\n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
\r | 回车(CR) ,将当前位置移到本行开头 | 013 |
\t | 水平制表(HT) (跳到下一个TAB位置) | 009 |
\ | 代表一个反斜线字符''' | 092 |
' | 代表一个单引号(撇号)字符 | 039 |
" | 代表一个双引号字符 | 034 |
? | 代表一个问号 | 063 |
格式化后的效果 | 动词 | 描述 |
---|---|---|
[0 1] | %v | 缺省格式 |
[]int64{0, 1} | %#v | go语法打印 |
[]int64 | %T | 类型打印 |
格式化后的效果 | 动词 | 描述 |
---|---|---|
15 | %d | 十进制 |
+15 | %+d | 必须显示正负符号 |
␣␣15 | %4d | Pad空格(宽度为4,右对齐) |
15␣␣ | %-4d | Pad空格 (宽度为4,左对齐) |
1111 | %b | 二进制 |
17 | %o | 八进制 |
f | %x | 16进制,小写 |
Value: 65 (Unicode letter A)
格式化后的效果 | 动词 | 描述 |
---|---|---|
A | %c | 字符 |
'A' | %q | 有引号的字符 |
U+0041 | %U | Unicode |
U+0041 'A' | %#U | Unicode 有引号 |
Value: 1234.567
格式化后的效果 | 动词 | 描述 |
---|---|---|
1.234560e+02 | %e | 科学计数 |
123.456000 | %f | 十进制小数 |
Value: "cafe"
格式化后的效果 | 动词 | 描述 |
---|---|---|
cafe | %s | 字符串原样输出 |
␣␣cafe | %6s | 宽度为6,右对齐 |
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) //此处会报错
}
//和 C 语言的 for 一样:
for init; condition; post { }
//和 C 的 while 一样:
for condition { }
//和 C 的 for(;;) 一样:
for { }
for语句执行过程如下:
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)
}
}
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++
}
}
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")
}
多处错误处理存在代码重复时是非常棘手的,例如:
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()
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 );
}
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("未知型")
}
}
不同的 case 表达式使用逗号分隔。
var a = "mum"
switch a {
case "mum", "daddy":
fmt.Println("family")
}
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}
查看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.
对于Python列表-我们可以由此得出结论,对于每个新元素,我们需要另外八个字节来引用新对象。新的整数对象本身消耗28个字节。列表“ lst”的大小,不包含元素的大小,可以使用以下公式计算:
64 + 8 * len(lst) + + len(lst) * 28
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
数组还有一个特性:数组的大小一开始需要指定好,后期动态扩充会拷贝值
对于机器学习的同学来说:数组用的更多,一般都是用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)
机器学习的同学对python的array应该很了解,但是做web开发的同学对python的array使用的不多
Go 语言提供了数组类型的数据结构。 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。
数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从0开始,到长度减1。
数组一旦定义后,大小不能更改。
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。
相对于去声明 number0, number1, ..., number99 的变量,使用数组形式 numbers[0], numbers[1] ..., numbers[99] 更加方便且易于扩展。
数组元素可以通过索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。
需要指明数组的大小和存储的数据类型。
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])
}
}
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)
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大
Go的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。 本文将深入切片的本质,并讲解它的用法。
// 第一种
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)
s :=[] int {1,2,3 }
直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s := arr[:]
创建 slice 的方式有以下几种:
序号 | 方式 | 代码示例 |
---|---|---|
1 | 直接声明 | var slice []int |
2 | new | slice := *new([]int) |
3 | 字面量 | slice := []int{1,2,3,4,5} |
4 | make | slice := make([]int, 5, 10) |
5 | 从切片或数组“截取” | slice := array[1:5] ** 或 **slice := sourceSlice[1:5] |
append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。
append 函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。
使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。
这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。
当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍 原 slice 容量超过 1024,新 slice 容量大家可以通过源码了解, 文章推荐:
要求所有的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在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)
}
// 创建
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
}
●遍历的顺序是随机的
●使用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值
}
由于遍历的时候,遍历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
}
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分布式爬虫", ""}
var c *Course = new(Course)
注意 new() 函数返回的是指针类型。下面再引入结构体变量的第四种创建形式,这种形式也是零值初始化,就数它看起来最不雅观。
var c Course
最后我们再将三种零值初始化形式放到一起对比观察一下
var c1 Course = Course{}
var c2 Course
var c3 *Course = new(Course)
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)
}
通过观察 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
}
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)
}
结构体作为一种变量它可以放进另外一个结构体作为一个字段来使用,这种内嵌结构体的形式在 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语言支持鸭子类型
所谓的继承仅仅是形式上的语法糖,c.show() 被转换成二进制代码后和 c.Point.show() 是等价的,c.x 和 c.Point.x 也是等价的。
结构体的字段除了名字和类型外,还可以有一个可选的标签(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)
}
}
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 次内存读取操作,第一步是取得指针地址,第二部是读取地址的内容,它比值访问要慢。但是在方法调用时,指针传递可以避免结构体的拷贝操作,结构体比较大时,这种性能的差距就会比较明显。
还有一些特殊的结构体它不允许被复制,比如结构体内部包含有锁时,这时就必须使用它的指针形式来定义方法,否则会发生一些莫名其妙的问题。
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")
错误和异常是两个不同的概念,非常容易混淆。很多程序员习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。从表面上看,一切皆错误的思路更简单,而异常的引入仅仅增加了额外的复杂度。
但事实并非如此。众所周知,Golang遵循“少即是多”的设计哲学,追求简洁优雅,就是说如果异常价值不大,就不会将异常加入到语言特性中。
错误和异常处理是程序的重要组成部分,我们先看看下面几个问题:
错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中 ;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是 。
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错误和异常是可以互相转换的:
什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。
在这个启示下,我们给出异常处理的作用域(场景):
其他场景我们使用错误处理,这使得我们的函数接口很精炼。对于异常,我们可以选择在一个合适的上游去recover,并打印堆栈信息,使得部署后的程序不会终止。
说明: Golang错误处理方式一直是很多人诟病的地方,有些人吐槽说一半的代码都是"if err != nil { / 打印 && 错误处理 / }",严重影响正常的处理逻辑。当我们区分错误和异常,根据规则设计函数,就会大大提高可读性和可维护性。
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。
很多人写代码时,到处return errors.New(value),而错误value在表达同一个含义时也可能形式不同,比如“记录不存在”的错误value可能为:
这使得相同的错误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")
根据笔者经验,层层都加日志非常方便故障定位。
说明:至于通过测试来发现故障,而不是日志,目前很多团队还很难做到。如果你或你的团队能做到,那么请忽略这个姿势:)
我们一般通过判断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
}
通常,当函数返回non-nil的error时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read函数会返回可以读取的字节数以及错误信息。对于这种情况,应该将读取到的字符串和错误信息一起打印出来。
去年学习Erlang的时候,建立了速错的理念,简单来讲就是“让它挂”,只有挂了你才会第一时间知道错误。在早期开发以及任何发布阶段之前,最简单的同时也可能是最好的方法是调用panic函数来中断程序的执行以强制发生错误,使得该错误不会被忽略,因而能够被尽快修复。
我们在调用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()
}
当某些不应该发生的场景发生时,我们就应该调用panic函数来触发异常。比如,当程序到达了某条逻辑上不可能到达的路径:
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}
入参不应该有问题一般指的是硬编码,我们先看“一个启示”一节中提到的两个函数(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 {/ 打印 && 错误处理 /}"代码块)。