没有理想的人不伤心

Golang - 基础语法介绍

2025/03/01
2
0

image.png

go 官方教程

https://tour.go-zh.org/list

一、入门

go 是编译型语言,源文件总是按 UTF-8 编码

$  go run helloworld.go
#对以.go 为后缀的源文件进行编译、链接,运行生成的可执行文件。

$  go build helloworld.go
#生成了一个叫 helloworld 的二进制程序,随时可以执行---->  ./helloworld

go 对代码的格式化非常严格,提供 gofmt 工具将代码以标准格式重写,养成使用 gofmt 工具的习惯。

//导入多个包时采用列表形式
import(
    "fmt"
	"os"
)

var 用来声明变量,可以在声明时初始化,若无明确的初始化,则隐式地初始化为这个类型的空值—> 数字:0 字符串:“”

go 中的i++是语句,因此不能使用j=i++,也不能前置**++i**

左大括号**{**不能单独成行,只能如 func main() {

s:= ""    
var s string
var s = ""
var s string = ""
//以上四种声明变量方式等价,但后面两种多此一举,推荐使用前两种
//:= 是短变量声明符

空标识符_(下划线),可以用在任何语法需要变量名但程序逻辑不需要的地方,如丢弃每次迭代产生的无用的索引。

Printf()以特定格式化输出,且默认没有换行符,需要自己手动使用转义字符\n,可以使用%v 打印任意类型的值

%+v:较详细的值

%#v:更加详细的值

1730535288520-6ab93196-e973-4a6d-81c5-0f5a692484c4.png

而 Println 则使用%v(内置格式的任何值)的方式格式化参数,并在最后追加换行符

二、基础

命名

go 中的函数、变量、常量等实体根据第一个字母的大小写来区分其是否能跨包访问

首字母大写则它是导出的,意味着它对包外是可见可访问的。

如:fmt 包中的 Printf

若实体声明在函数内,则只在函数局部有效;若声明在函数外,则对包里的所有源文件可见,即包里其他的源文件都可以调用该实体。

声明

实体的声明:

  • 变量 var
  • 常量 const
  • 类型 type
  • 函数 func

在每个.go 的源文件中,必须先以 package 声明文件属于哪个包,再是 import 声明。

go 的零值机制:声明的一个变量若没有赋值,则默认其声明类型下的零值。

特殊的,零值对于接口和引用类型(slice、指针、map、通道、函数)为nil

对于数组、结构体这样的复合类型,零值是其所有元素或成员的零值

声明时,指定其类型或赋值必须有其一

var a,b,c = true,2.3,“abcd”

像上面,可以声明一个变量列表进行多个不同类型变量的初始化

变量也可以通过调用返回多个值的函数进行初始化

var f,err = os.Open(name)

os.Open 返回一个文件和一个错误

短变量声明:

在函数中,可选用短变量声明来声明和初始化局部变量,当然也可用 var 进行声明

格式:name:= expression

name 的类型由 expression 的类型决定

多个变量也可以用短变量声明一起初始化,其中可以有已声明的变量,对该变量相当于赋值,但短变量声明最少声明一个新的变量,否则编译失败

i,j:= “abcx”,1

区分:

表达式:i,j = j,i //交换 i、j 变量的值,这个为多重赋值,允许多个变量一起被赋值。

类型声明 type:

类型的声明通常出现在包级别,若首字母大写则可以导出

可以定义一个新的命名类型,他和已有类型使用相同的底层类型

type name underlying-type

//为了说明类型声明,我们把不同计量单位的温度值转换为不同的类型
package temperature
import fmt
type C float64 	//摄氏度
type F float64	//华氏度
const(
    zeroc C = -273.15
    freezec C = 0
    boilc C = 100
)
func CToF(c C)F { return F(c*9/5 + 32) }  
//F()为显式类型转换,不是函数
func FToC(f F)C { return C(f - 32) * 5 / 9}

上述代码中即使底层的类型是相同的,但他们不是相同的类型,不能对他们一起使用算术表达式进行比较和合并。

区分这些类别可以防止无意间合并不同计量单位的温度值

对于每一个类型 T,都有一个对应的类型转换操作 T(x)

两个类型具有相同的底层类型或二者都是指向相同底层类型变量的未命名指针时,则,两种类型才可以相互转换

如数字类型间的转换;字符串和一些 slice 类型间的转换。

指针

和 c 语言差不多,&取地址符

指针类型的零值为 nil

p!= nil 为 true 则指针 p 的指向不为空

每一个聚合类型变量的组成(结构体或数组中的元素)都是变量,所以都有一个地址

可以通过地址引用通过函数更新间接传递的变量值

func incr(p *int)int{
    *p++
    return *p
}

v:= 1
incr(&v) 	 	//v 现在等于 2
fmt.Println(incr(&v))   //v 现在等于 3

new 函数

创建变量的另一种方式使用内置的 new 函数

表达式 new(T)创建一个未命名的 T 类型变量,初始化为 T 类型的零值,并返回其地址(地址类型为*T)

p:= new(int)	//p 为*int 类型的指针变量,指向未命名的 int 型变量
fmt.Println(*p) 	//输出 0
*p = 10
fmt.Println(*p)		//输出 10

用 new 只是在语法上的便利,不需要引入一个虚拟的变量名

每一次调用 new 都会返回不同的地址

new 是一个预声明函数,不是一个关键字!

赋值

隐式的赋值:

如:函数的 return、slice 赋值

medals:= []string{“a”,“b”,“c”}

相当于

medals[0] = “a”

medals[1] = “b”

medals[2] = “c”

赋值只有在值对于变量类型是可赋值时才合法,即类型必须精准匹配,不能牛头不对马嘴。

nil 可以被赋给任何的接口变量或引用类型

三、基本数据

go 的数据类型

  1. 基础类型:数字、字符串、布尔型
  2. 聚合类型:数组、结构体
  3. 引用类型:指针、slice、map、函数、通道
  4. 接口类型

数字

整数

有符号整数:int8、int16、int32、int64

无符号整数:uint8、uint16、uint32、uint64、uintptr

uintptr 大小不明确,足以完整的存放指针,仅用于底层编程,如 go 与 c 或操作系统的接口界面

常用:int、uint

特殊:rune 类型(和 int32 是同义词)、byte 类型(和 uint8 是同义词),名字可互换使用

rune 常用于指明一个值是 Unicode 码点

byte 强调一个值是原始数据

int 类型有别于 int32 类型,要通过 int()或 int32()进行显式转换,uint、uintptr 也是一样,有别于其大小明确的相似类型。

运算:
%取模运算只能用于整数

对于整数+x 是 0+x 的简写,-x 是 0-x 的简写

对于浮点数和复数,+x 就是 x,-x 就是 x 的负数

左移运算 x<<n 相当于 x 乘以 2 的 n 次

右移运算 x>>n 相当于 x 除以 2 的 n 次

在格式化输出中,通常有多少个%就要提供相同个数的操作数,

但在%后加副词[n],可以指定使用第几个操作数,这样就可以重复使用操作数

o:= 06666
fmt.Printf("%d %[1]o %#[1]o\n",0)   //输出:438 666 0666
//都指定第一个操作数,%o 为八进制,%x、%X 为十六进制,
//%后面加#表示输出八进制、十六进制的前缀,即 0 和 0x

浮点数

浮点数数据类型:float32、float64

浮点数的格式化输出:

%g:自动保持足够的精度,并选择简洁的表示方式

%f:无指数

%e:有指数

以上三种都能掌控输出宽度和数值精度

math 包中定义了大量特殊值:

  • math.MaxFloat32:float32 的最大值
  • math.MaxFloat64:float64 的最大值
  • +Inf:正无穷大
  • -Inf:负无穷大
  • math.NaN:表示数学上无意义的运算结果。(如 0/0)
var z float64
fmt.Println(z,-z,1/z,-1/z,z/z)
//输出:0 -0 +Inf -Inf NaN

复数

数据类型:complex64、complex128

通过内置的 complex 函数根据给定实部和虚部来创建复数,

内置的 real 和 imag 函数用来提取复数的实部和虚部。

var x complex128 = complex(1,2)	//1+2i
var y complex128 = complex(3,4)	//3+4i
fmt.Println(x*y)				//-5+10i
fmt.Println(real(x*y))			//-5
fmt.Println(imag(x*y))			//10

math/cmplx包提供了复数运算所需的库函数


布尔值

数据类型:bool

布尔值可由运算符&&、||组合运算,且&&比||的优先级高。

这可能引起短路行为:若运算符左边的操作数可以直接确定运算总体结果,则右边的操作数将不会进行。

如:s!= “” && s[0] == ‘x’

s 若为空字符串,则不会再去判断 s[0]是否等于 0,只有 s 不为空,才会判断

布尔值无法隐式的转换成数字(0、1),要转换则需要使用 if 进行转换,不像 c 语言中,0 就是 false,非 0 就是 true


字符串

字符串是不可变的字节序列,可包含任意数据。

内置的 len 函数返回字符串的字节数,不是文字符号的数目!!

可通过字符串数组下标访问的方式对字符串中的字节进行访问。

第 i 个字节并不一定是第 i 个字符。

子串生成操作:s[n:m]生成一个新的字符串,下表不能越界,n 和 m 的默认值为起始值和终止值,可以省略。

+加号运算符:连接两个字符串而生成一个新的字符串

字符串可以通过比较运算符==、<等进行比较,按字符串变量的字节顺序进行

字符串不可改变,内部的数据不允许修改。如:s[0]=“l” 编译出错

字符串和字节 slice 相互转换:

s:= "abc"
a:= []byte(s)
b:= string(a)

与字符串操作相关标准包:

  • strings
  • bytes:用于处理字节 slice
  • strconv:主要用于字符串类型和其他类型的相互转换
  • unicode

字符串和数字转换:


常量

常量本质上都属于基本数据类型:数字、字符串或布尔型

可以在同一声明中用列表的形式定义一系列常量

const(
    e = 2.7
    pi = 3.14
)

常量的声明可以使用常量生成器 iota,它创建一系列相关值,而不是逐个值显示写出

iota 从 0 开始取值,逐项加 1

type Weekday int

const(
    Sunday Weekday = iota
    Monday 
    Tuesday
    wednesday
    Thursday
    Friday
    Saturday
)
//定义了 Weekday 具体类型,这种类型通常称为枚举型

无类型常量:

四、复合数据类型

数组、slice、map、结构体

数组

数组:

  • 具有固定长度,长度不可变
  • 元素数据类型相同
var a [3]int	//数组的声明,没有初始化则默认零值
var q [4]int = [4]int{1,2,3,4}	//使用数组字面量进行初始化
var p [4]int = [4]int{1,2}		//p[2]、p[3]默认为零值

//数组字面量中,省略号出现在数组长度位置,数组长度由初始化数组个数决定
q:= [...]int{1,2,3}			
//指定索引的初始化
r:= [...]int{99:-1}	
//定义了一个拥有 100 个元素的数组,只有最后一个元素是-1,其他的都是 0

函数的传参、数组之间的比较都要求数组长度一样长,否则编译错误

slice

  • 元素拥有相同数据类型
  • 可变长度的序列

通常写成 **[]T **,T 表示数据类型

slice 有三个属性:指针、长度、容量

  • 指针指向数组的第一个可以从 slice 中访问的元素
  • 长度是 slice 的长度,用 len 函数返回长度
  • 容量是从 slice 的起始元素到底层数组的最后一个元素间的个数,用 cap 函数返回

slice 操作符 s[i:j]创建了一个新的 slice,索引为[i,j)左闭右开,元素个数为 j-i,s 可以是数组也可以是 slice

可以在 slice 容量范围内扩展 slice,即新的 slice 比原 slice 长,但是长度不能超过原 slice 的容量,否则会导致程序宕机

a:= [...]int{9:-1}
b:= a[5:7]		//b 的容量为 5,长度为 2
fmt.Println(b[:6])	//新的 slice 的长度为 6,大于 b 的容量
//宕机,超过了被引用对象的边界,
c:= b[2:4]	//c 在 b 的容量范围内扩展了 b

字符串(string)子串操作和对字节 slice([]byte)做 slice 操作几乎相同,都写做 x[m:n]

区别在于:若 x 是字符串,x[m:n]返回一个字符串,若 x 是字节 slice,则 x[m:n]返回字节 slice。

由于 slice 包含指向数组元素的指针,所以将一个 slice 传递给函数时,可以在函数内部修改底层数组的元素(相当于传入地址的引用传递),创建一个 slice 等于为数组创建了一个别名。

切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素。

与它共享底层数组的切片都会观测到这些修改。

//反转一个整型 slice 中的元素
func reverse(s []int) {
    for i,j: =0,len(s)-1;i<j;i,j = i+1,j-1 {
        s[i],s[j] = s[j],s[i]
    }
}
//反转数组 a
a:= [...]int{0,1,2,3,4,5}
reverse(a[:])
fmt.Println(a)	//[5,4,3,2,1,0]

slice 初始化和数组初始化的区别:

s:= []int{1,2,3,4,5} //slice

s:= […]int{1,2,3,4,5} //数组

slice 不用指定长度

和数组不同的是,slice 无法作比较,不能直接用"=="来比较两个 slice 中是否拥有相同元素,需要自己写函数来比较

slice 唯一允许的直接比较是和 nil 进行比较

slice 类型的零值是 nil,值为 nil 的 slice 没有对应的底层数组,其长度和容量都为 0

slice 可以包含任何元素,甚至是其他的 slice

// 创建一个井字板(经典游戏)
// 二维数组
	board:= [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

make 创建 slice

内置函数 make 可以创建一个具有指定元素类型、长度、容量的 slice,其中容量可以省略,此时长度和容量相等。

make([]T,len)	//返回的 slice 引用了整个数组
//或
make([]T,len,cap)	
//slice 只引用了数组前 len 个元素,数组长度就是 slice 的容量
//和 make([]T,cap)[:len]功能相同

其实 make 创建了一个无名数组并返回了它的一个 slice,并且该数组仅能用这个 slice 访问

append 函数

用来将元素追加到 slice 后面

var runes []rune
for _,r := range"hello world" {
    runes = append(runes,r)
}
fmt.Printf("%q\n",runes)
//"['h' 'e' 'l' 'l' 'o' 'w' 'o' 'r' 'l' 'd']"

使用 append 函数,若 slice 的长度达到容量,则会创建一个拥有足够容量的新的底层数组来存储新的元素。

通常情况下,我们不知道一次 append 调用会不会导致一次新的内存分配,因此我们不能假设原始的 slice 和调用 append 后返回的 slice 指向同一底层数组,也无法证明他们指向不同的底层数组,我们也无法假设旧 slice 上对元素的操作会不会影响新的 slice 元素。

因此通常将 append 调用结果再次赋值给传入 append 函数的 slice

即:runes = append(runes,r)

对于任何函数,只要可能改变 slice 的长度或者容量,或者使 slice 指向不同的底层数组,都需要更新 slice 变量

为了正确使用 slice,必须记得,虽然底层数组的元素是间接引用的,但是 slice 的指针、容量和长度不是,从这个角度看,slice 并不是纯引用类型,而是像下面的这种聚合类型:

type IntSlice struct {
	ptr	*int
	len, cap int
}

append 可以同时给 slice 添加多个元素甚至另一个 slice 中的所有元素

var x []int
x = append(x,1,2,3,4)
x = append(x,x...)
//追加 x 中的所有元素,x 后的省略号表示如何将一个 slice 转换为参数列表的机制
fmt.Println(x)
//"[1,2,3,4,1,2,3,4]"

map

散列表,是一个拥有键值对元素的无序集合

在这个集合中,键是唯一的,键对应的值可以通过键来获取、更新或者移除

map 的类型是"map[K]V",其中 K 和 V 是字典的键和值对应的数据类型

键的类型 K,必须是可以通过比较符==来比较的数据类型,所以 map 可以检测一个键是否存在

值类型没有任何限制

map 类型的零值是 nil

内置函数 make 可以用来创建一个 map

//创建一个从 string 到 int 的 map
ages:= make(map[string]int)
//也可使用 map 字面量创建一个带初始化键值对元素的字典
ages:= map[string]int{
    "alice" :33
    "caoziheng" :23
}
//另一种新的空 map 表达式
ages:= map[string]int{}
var ages map[string]int	//	为零值 nil,不能为零值 map 赋值

//内置函数 delete 从字典中根据键移除一个函数
delete(ages,"alice")
//若对应键不存在则返回 “值类型”的零值

//使用 for 和 range 关键字来获取 map 中的所有键和值
for name,age:= range ages{
	...
}

map 是散列表的引用,不是变量,不能获取它的地址

map 中元素迭代的顺序是不固定的,在实践中认为是随机的,若需要按某种顺序来遍历 map 中的元素,必须显式的给键排序,

map 不能直接用==进行比较,能且仅能和 nil 进行比较

结构体

结构体是将零个或者多个任意类型的命名变量组合在一起的聚合数据结构

每个变量都叫做结构体的成员

type People struct {
    name,race string	//相同数据类型可以写在一行
    age int
}
var caoziheng People
caoziheng.name = czh
caoziheng.age = 22
//可以获取成员变量的指针,通过指针访问
age:= &caoziheng.age
*age = 0 + *age
//结构体指针也可以用点号访问变量
var Chinese *People = &caoziheng
Chinese.age += 1	//相当于(*Chinese).age += 1

如果一个成员变量的首字母大写,则该变量是可导出的

一个结构体可以同时拥有可导出和不可导出的成员变量

一个聚合类型不能包含他自己(数组也适用),即结构体 s 中不能再定义一个成员变量 s,但是可以定义一个 s 的指针类型,即*s

没有任何成员变量的结构体称为空结构体,写作 struct{},它没有长度,也不携带任何信息,也没啥用

结构体初始化

//第一种,一般适用于小的结构体中,不推荐
type Point struct { x,y int}
p:= Point{1,2}
//要求按照正确的顺序,为每个成员变量赋值

//第二种
p:= Point{x:1,y:2}
p:= Point{y:2}
//通过指定部分或者全部成员变量的名称和值来初始化,没有指定的就是这个类型的零值
//成员变量的顺序也就无所谓了

//更简单的方式初始化结构体指针
PP:= &Point{1,2}

出于效率考虑,通常使用结构体指针的方式传递给函数或作为函数返回值

结构体比较

若结构体所有成员变量都是可比较的,则该结构体就是可比较的,即可以用==

结构体嵌套和匿名成员

go 的结构体嵌套机制让我们可以将一个命名结构体当作另一个结构体类型的匿名成员使用,并提供了一种方便的语法,使用简单的表达式(如 x.f)就可以代表连续的成员(比如 x.d.e.f)

go 允许我们定义匿名成员,只需要指定元素类型即可,且该类型必须是一个命名类型或指向该类型的指针

type Point struct {
    X,Y int
}
type Circle struct {
    Point
    Radius int
}
type Wheel struct {
    Circle
    Spokes int
}
//有了这种结构体嵌套功能,才能直接访问到我们所需要的变量而不是指定一大串中间变量
var w Wheel
w.X = 8		//等价于 w.Circle.Point.X = 8
w.Y = 8		//等价于 w.Circle.Point.Y = 8
w.Radius = 10	//等价于 w.Circle.Radius = 10
w.Spokes = 100

上面注释中的写法也是正确的,匿名成员拥有隐式名字,就是对应的结构类型本身的名字,只是访问最终需要的变量时可以省略中间的所有匿名成员

由于匿名成员也拥有隐式名字,所以不能在同一个结构体中定义两个类型相同的匿名成员

含有匿名成员的结构体没有快捷方式进行初始化

//以下方式无法通过编译,编译错误,未知成员变量
w = Wheel{8,8,10,100}
w = Wheel{X:8,Y:8,Radius:10,Spokes:100}

//只能使用下面的方式进行初始化
w = Wheel{Circle{Point{8,8},10},100}
w = Wheel{
    Circle:Circle {
        Point:Point{X:8,Y:8},
    	Radius:10
    },
    Spokes: 100,	//注意:单独成行时尾部的逗号是必须滴,Radius 也一样
}

以快捷方式访问匿名成员变量的方式也适用于访问匿名成员的内部方法,外围的结构体类型获取的不仅仅是匿名成员的内部变量还有相关方法,这个机制就是从简单类型对象组合成复杂的复合类型的主要方式。

结构体指针

结构体字段可以通过结构体指针来访问。

如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。


JSON

JavaScript 对象表示法(JSON)是一种发送和接受格式化信息的标准。

JSON 不是唯一的标准,XML、ASN.1 等都是相似的标准

Go 通过标准库 encoding/json、encoding.xml、encoding/asn1 以及其他库对这些格式的编解码提供支持

JSON 最基本的类型是数字、布尔值和字符串,使用反斜杠作为转义字符

这些基础类型可以通过 JSON 的数组和对象进行组合,JSON 的数组是一个有序的元素序列,用来编码 go 里的数组和 slice,JSON 的对象是一个从字符串到 值的映射,写成 name:value 的形式,用来编码 go 的 map 和结构体

boolean		true
number		123
string"caoziheng niubi"
array			["gold","silver","blue"]
object		{"name": "caoziheng",
						"age":22
						"hobby": ["basketball","running"]}

Go ==> JSON 称为 marshal

JSON ==> Go 称为 unmarshal

type chinese struct {
	Name  string
	Age   int
	Hobby []string
}
//只有可导出的成员才能转换为 JSON 字段,因此成员定义中首字母要大写
var chineses = []chinese{
	{Name: "caoziheng",Age:22,Hobby: []string{"basketball", "run"}},
	{Name: "mayun",Age:50,Hobby: []string{"nomoney", "music"}},
	{Name: "mahuateng",Age:48,Hobby: []string{"wangzherongyao", "hepingjingying"}},
}

//把 chineses 结构体转为 JSON
data,err := json.Marshal(chineses)
if err!= nil {
	log.Fatalf("JSON marshaling failed: %s",err)
}
fmt.Printf("%s\n",data)

上面的输出 JSON 格式为:

[{“Name”:“caoziheng”,“Age”:22,“Hobby”:“basketball”,“run”]},“Name”:“mayun”,“Age”:50,“Hobby”:“nomoney”,“music”]},“Name”:“mahuateng”,“Age”:48,“Hobby”:“wangzherongyao”,“hepingjingying”]}]

marshal 使用 go 结构体成员的名称作为 JSON 对象里面字段的名称(通过反射)

只有可导出的成员才可以转换为 JSON 字段,也就是成员名称的定义首字母大写

上面的表示方法虽然包含了所有信息,但难以阅读,为方便阅读,有一个”json.MarshalIndent“的变体可以输出整齐格式化过的结果

该函数有两个参数,

  • 定义每行输出的前缀字符串
  • 定义缩进的字符串
data,err := json.MarshalIndent(chineses, "", "	")
if err!= nil {
	log.Fatalf("JSON marshaling failed: %s",err)
}
fmt.Printf("%s\n",data)

//格式化后的 JSON 表达如下
[
	{
		"Name": "caoziheng",
		"Age":22,
		"Hobby": [
			"basketball",
			"run"
		]
	},
	{
		"Name": "mayun",
		"Age":50,
		"Hobby": [
			"nomoney",
			"music"
		]
	},
	{
		"Name": "mahuateng",
		"Age":48,
		"Hobby": [
			"wangzherongyao",
			"hepingjingying"
		]
	}
]

JSON ==> GO

由 json.Unmarshal 实现,通过合理的定义 Go 的数据结构,可以选择将那部分 JSON 数据解码到结构体对象中

下面代码将 JSON 数据转换到结构体 slice 中,这个结构体唯一的成员就是一个字符串 slice

var hobbies []struct{ Hobby []string }	//结构体成员依然要求可导出!
if err:= json.Unmarshal(data, &hobbies);err != nil {
	log.Fatalf("JSON unmarshaling failed: %s",err)
}
fmt.Println(hobbies)

//Unmarshal 结果如下,只包含兴趣 slice
[{[basketball run]} {[nomoney music]} {[wangzherongyao hepingjingying]}]

五、分支结构

if、else、else if 后的条件不需要加括号

循环结构只有 for

switch 功能强大,且默认每个 case 最后都有 break,不需要手动添加 break

switch 可以用任意的结构类型

甚至在 switch 可以不加任何变量,在 case 中加入条件,来取代繁杂的 if、elseif 语句

1730534448877-4d40e7f4-4a47-40c0-b7e6-9bc008f09a77.png

六、函数

函数声明

func name(参数列表) (返回值列表){
    函数体
}

每个函数声明都包含一个函数名,一个形参列表,一个可选的返回值列表以及函数体

形参列表指定了一组变量的参数名和参数类型,返回列表指定了函数返回值的类型

返回值也可以像形参一样命名,此时,每个命名的返回值会声明为一个局部变量,且初始化为其类型的零值

如:

func add(x,y int) (z int) {
	z = x + y
	return		//指定了函数返回值的名称,return 后可以空着
}

返回列表可以没有,即没有返回值,有多个返回值时才需要用括号()括起来

函数的类型称作函数签名,当两个函数拥有相同的形参列表和返回列表时,认为这两个函数的类型或者函数签名时相同的,而形参和返回值的名字不会影响函数类型

可能会偶尔看到有些函数的声明没有函数体,说明该函数使用了除 Go 以外的语言实现,这样的声明定义了该函数的签名

package math

func Sin(x float64)float64		//使用汇编语言实现

递归

递归:函数直接或者间接的调用自己

多返回值

在 Go 中,一个函数可以返回多个值

调用多返回值函数时,返回给调用者的是一组值,调用者必须显式的将这些值分配给变量

如果某个值不被使用,可以将其分配给 blank identifier

如:links, _ := findLinks(url) // errors ignored

一个函数内部可以将另一个有多返回值的函数调用作为返回值

func findLinksLog(url string) ([]string,error) {
    log.Printf("findLinks %s",url)
    return findLinks(url)
}

当你调用接受多参数的函数时,可以将一个返回多参数的函数调用作为该函数的参数

log.Println(findLinks(url))
等价于
links,err := findLinks(url)
log.Println(links,err)

函数的最后一个 bool 类型的返回值表示函数是否运行成功,error 类型的返回值代表函数的错误信息,对于这些类似的惯例,我们不必思考合适的命名,它们都无需解释。

如果一个函数所有的返回值都有显式的变量名,那么该函数的 return 语句可以省略操作数。这称之为 bare return 裸返回。(不宜过度使用,会难以理解)

错误

在 Go 的错误处理中,错误是软件包 API 和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一。

对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。如果导致失败的原因只有一个,额外的返回值可以是一个布尔值,通常被命名为 ok

若有多个原因,则使用 error 类型,内置的 error 是接口类型

error 类型可能是 nil 或者 non-nil。nil 意味着函数运行成功,non-nil 表示失败。对于 non-nil 的 error 类型,我们可以通过调用 error 的 Error 函数或者输出函数获得字符串类型的错误信息。

fmt.Println(err)
fmt.Printf("%v",err)

通常,当函数返回 non-nil 的 error 时,其他的返回值是未定义的(undefined),这些未定义的返回值应该被忽略。然而,有少部分函数在发生错误时,仍然会返回一些有用的返回值。比如,当读取文件发生错误时,Read 函数会返回可以读取的字节数以及错误信息。对于这种情况,正确的处理方式应该是先处理这些不完整的数据,再处理错误。因此对函数的返回值要有清晰的说明,以便于其他人使用。

在 Go 中,函数运行失败时会返回错误信息,这些错误信息被认为是一种预期的值而非异常(exception)

Go 使用控制流机制(如 if 和 return)处理错误,而非抛出异常的方式

错误处理策略

处理错误有常见的 5 种方式

  1. 传播错误的方式,函数中某个子程序的失败,会变成该函数的失败

编写错误信息时,我们要确保错误信息对问题细节的描述是详尽的。尤其是要注意错误信息表达的一致性,即相同的函数或同包内的同一组函数返回的错误在构成和处理方式上是相似的。由于错误信息经常是以链式组合在一起的,所以错误信息中应避免大写和换行符。

  1. 如果错误的发生是偶然性的,或由不可预知的问题导致的。则可以重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
// WaitForServer attempts to contact the server of a URL.
// It tries for one minute using exponential back-off.
// It reports an error if all attempts fail.
func WaitForServer(url string)error {
    const timeout = 1 * time.Minute
    deadline:= time.Now().Add(timeout)
    for tries:= 0;time.Now().Before(deadline);tries++ {
        _,err := http.Head(url)
        if err == nil {
            return nil // success
        }
        log.Printf("server not responding(%s);retrying…",err)
        time.Sleep(time.Second << uint(tries)) // exponential back-off
    }
    return fmt.Errorf("server %s failed to respond after %s",url,timeout)
}
  1. 输出错误信息并结束程序,这种策略只应在 main 中执行

对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了 bug,才能在库函数中结束程序。

// (In function main.)
if err:= WaitForServer(url);err != nil {
    fmt.Fprintf(os.Stderr, "Site is down: %v\n",err)
    os.Exit(1)
}
等同于
if err:= WaitForServer(url);err != nil {
    log.Fatalf("Site is down: %v\n",err)
}
//log 中的所有函数,都默认会在错误信息之前输出时间信息。
  1. 只需要输出错误信息,不需要中断程序的运行

可以通过 log 包提供函数或者标准错误流输出错误信息。

if err:= Ping();err != nil {
    log.Printf("ping failed: %v;networking disabled",err)
}
或者
if err:= Ping();err != nil {
    fmt.Fprintf(os.Stderr, "ping failed: %v;networking disabled\n",err)
}
  1. 直接忽略掉错误

我们应该在每次函数调用后,都养成考虑错误处理的习惯,当你决定忽略某个错误时,你应该清晰地写下你的意图。

在 Go 中,错误处理有一套独特的编码风格。检查某个子函数是否失败后,我们通常将处理失败的逻辑代码放在处理成功的代码之前。如果某个错误会导致函数返回,那么成功时的逻辑代码不应放在 else 语句块中,而应直接放在函数体中

文件结尾错误(EOF)

io 包保证任何由文件结束引起的读取失败都返回同一个错误——io.EOF,该错误在 io 包中定义:

package io

import"errors"

// EOF is the error returned by Read when no more input is available.
var EOF = errors.New("EOF")

调用者只需通过简单的比较,就可以检测出这个错误。

如:从标准输入中读取字符,以及判断文件结束

in:= bufio.NewReader(os.Stdin)
for {
    r, _,err := in.ReadRune()
    if err == io.EOF {
        break // 文件结束
    }
    if err!= nil {
        return fmt.Errorf("read failed:%v",err)
    }
    // ...use r…
}

函数变量

在 Go 中,函数被看作第一类值(first-class values):像其他值一样,函数变量也拥有类型,可以被赋值给其他变量、传递给函数或者从函数中返回。对函数变量(function value)的调用类似函数调用

func square(n int)int { return n * n }
func negative(n int)int { return -n }
func product(m,n int)int { return m * n }

f:= square
fmt.Println(f(3)) // "9"

f = negative
fmt.Println(f(3))     // "-3"
fmt.Printf("%T\n",f) // "func(int)int"

f = product // compile error:can't assign func(int,int)int to func(int)int

函数类型的零值是 nil(空值)。调用空值的函数值会引起 panic 错误(宕机)

var f func(int)int
f(3) // 此处 f 的值为 nil, 会引起 panic 错误

函数变量可以与空值相比较,但函数变量之间不可比较

var f func(int)int
if f!= nil {
    f(3)
}

函数变量使得函数不仅将数据进行参数化,还将函数的行为当作参数进行传递。

如:strings.Map 对字符串中的每个字符调用 add1 函数,并将每个 add1 函数的返回值组成一个新的字符串返回给调用者

func add1(r rune)rune { return r + 1 }

fmt.Println(strings.Map(add1, "HAL-9000")) // "IBM.:111"
fmt.Println(strings.Map(add1, "VMS"))      // "WNT"
fmt.Println(strings.Map(add1, "Admix"))    // "Benjy"
func a(b int,c func(d int))

函数可以作为参数或者返回值传递

// 函数作为其他函数的参数进行传递,然后在其他函数内调用执行一般称为“回调”
func main() {
    callback(1,Add) //输出 The sum of 1 and 2 is:3
}

func Add(a,b int) {
    fmt.Printf("The sum of %d and %d is: %d\n",a,b,a+b)
}
// 第二个参数的类型是函数类型 func(int,int)
func callback(y int,f func(int,int)) {
    f(y,2) // this becomes Add(1,2)
}

函数签名

函数参数、返回值以及它们的类型被统称为函数签名

如果两个函数的参数列表和结果列表中的元素顺序及其类型是一致的,那么他们拥有相同的函数签名,也就是实现了同一类型的函数。

// 声明了一个函数类型 Printer
type Printer func(contents string) (n int,err error) 
// 该函数与 Printer 拥有相同的函数类型
func printToStd(contents string) (bytesNum int,err error) {
  return fmt.Println(contents)
}

func main() {
// 声明一个函数变量,这里要是不声明函数类型,直接用 p:= printToStd 也可以
  var p Printer
  p = printToStd
  p("something")
}

匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中指定函数变量,在 func 后面没有函数的名字,它是一个表达式,他的值称为匿名函数

函数字面量允许我们在使用函数时,再定义它

如:之前的 strings.Map可以写成

strings.Map(func(r rune)rune { return r + 1 }, "HAL-9000")

更为重要的是,通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着里层函数可以使用外层函数中的变量

如:

// squares 返回一个匿名函数。
// 该匿名函数每次被调用时都会返回下一个数的平方。
func squares()func()int {
    var x int
    return func()int {
        x++
        return x * x
    }
}
func main() {
    f:= squares()
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
    fmt.Println(f()) // "16"
}
// 或者写成
func main() {
    f:= squares
    fmt.Println(f()()) // "1"
    fmt.Println(f()()) // "4"
    fmt.Println(f()()) // "9"
    fmt.Println(f()()) // "16"
}
// 上面如果调用函数时,使用 f(),则返回的只是函数值的地址

函数 squares 返回另一个类型为 func()int 的函数。对 squares 的一次调用会创建一个局部变量 x 并返回一个匿名函数。每次调用匿名函数时,该函数都会先使 x 的值加 1,再返回 x 的平方。第二次调用 squares 时,会创建第二个 x 变量,并返回一个新的匿名函数。新匿名函数操作的是第二个 x 变量。

squares 的例子证明,函数变量不仅仅是一串代码,还记录了状态。在 squares 中定义的匿名内部函数可以访问和更新 squares 中的局部变量,这意味着匿名函数和 squares 中,存在变量引用。这就是函数变量属于引用类型且函数变量不可比较的原因。Go 使用闭包(closures)技术实现函数值,Go 程序员也把函数变量叫做闭包。

下面再看一个闭包的例子。

func adder()func(int)int {
    res:= 0
    return func(x int)int {
        res = (res + x) * 2
        return res
    }
}

func main() {
    a:= adder()
    for i:= 0;i < 10;i++ {
        fmt.Println(a(i))
    }
}

闭包的意义:缩小变量作用域,减少对全局变量的污染。

没有闭包的时候,函数就是一次性买卖,函数执行完毕后就无法再更改函数中变量的值(应该是内存释放了);有了闭包后函数就成为了一个变量的值,只要变量没被释放,函数就会一直处于存活并独享的状态,因此可以后期更改函数中变量的值(因为这样就不会被 go 给回收内存了,会一直缓存在那里)。

通过这个例子,我们看到变量的生命周期不由它的作用域决定:squares 返回后,变量 x 仍然隐式的存在于 f 中。

匿名函数的调用和普通函数一样需要给定参数

func main(){
    j,k := func(i string) (int,error) {
		return fmt.Println(i)
	}("caoziheng")
	fmt.Println(j,k)

    func() {
        fmt.Println("1234")
    }()
}

可变参数函数

参数的数量可变

在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“…”,这表示该函数会接收任意数量的该类型参数。

func sum(vals ...int)int {
    total:= 0
    for _,val := range vals {
        total += val
    }
    return total
}

fmt.Println(sum())           // "0"
fmt.Println(sum(3))          // "3"
fmt.Println(sum(1,2,3,4)) // "10"

values:= []int{1,2,3,4}
fmt.Println(sum(values...)) // "10"

vals 被看作是类型为[] int 的切片。sum 可以接收任意数量的 int 型参数

在上面的代码中,调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个切片作为参数传给被调用函数。如果原始参数已经是切片类型,我们该如何传递给 sum?只需在最后一个参数后加上省略符

虽然在可变参数函数内部,…int 型参数的行为看起来很像切片类型,但实际上,可变参数函数和以切片作为参数的函数是不同的:

func f(...int) {}
func g([]int) {}
fmt.Printf("%T\n",f) // "func(...int)"
fmt.Printf("%T\n",g) // "func([]int)"

可变参数函数经常被用于格式化字符串。下面的 errorf 函数构造了一个以行号开头的,经过格式化的错误信息。函数名的后缀 f 是一种通用的命名规范,代表该可变参数函数可以接收 Printf 风格的格式化字符串。

func errorf(linenum int,format string,args ...interface{}) {
    fmt.Fprintf(os.Stderr, "Line %d: ",linenum)
    fmt.Fprintf(os.Stderr,format,args...)
    fmt.Fprintln(os.Stderr)
}
linenum,name := 12, "count"
errorf(linenum, "undefined: %s",name) // "Line 12:undefined:count"

interface{}表示函数的最后一个参数可以接收任意类型

延迟函数(deferred)调用

只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用(先进后出)

func main() {
	defer fmt.Println("world")
	fmt.Println("hello")
}
//返回 hello world

宕机(Panic)

宕机发生时,正常的程序会终止执行,goroutine 中的所有延迟函数会执行,然后程序异常退出,并留下一条日志消息,

日志消息包括宕机的值,往往代表某种错误消息,每一个 goroutine 都会在宕机时显示一个函数调用的栈跟踪消息

宕机的发生方式

  • 程序运行错误宕机
  • 调用内置的宕机函数

内置的宕机函数可以接受任何值作为参数,如果碰到不可能发生的状况(如语句执行到了逻辑上不可能到达的地方),调用内置宕机函数是最好的选择