一脚踢你进 Go 语言大门!入门者必看,万字长文,建议收藏!
@[toc]
您诸位好啊,我是无尘!
第一部分:一脚踢你进 Go 语言大门!
Ⅰ、基础不牢,地动山摇
1.第一个例子:Hello World
第一行 package main 代表当前的文件属于哪个包,package 是 go 语言生命包的关键字,main 是包名,main 包是一个特殊的包,代表此项目为一个可运行的应用程序,而不是一个被其他项目引用的库。
第二行 import "fmt" 是导入一个 fmt 包,import 是关键字
第三行 func main(){} 定义了一个函数,func 是关键字,main 是函数名,mian 是一个特殊函数,代表整个程序的入口,程序在运行时,会点调用 main 函数。
第四行 fmt.Println("Hello World") 是通过 fmt 包的 Println 函数打印 “Hello World”文本。
2.Go 环境搭建
可以从官网 https://golang.org/dl/ (国外官网)和 https://golang.google.cn/dl/ (国内官网)下载 Go 语言开发包。
2.1 环境变量
GOPATH:Go 项目的工作目录,现在有了 Go Module 模式,所以基本上用来放使用 go get 命令获取的项目
GOBIN:Go 编译生成的程序安装目录,比如
go install
命令 会把生成的 go 程序安装到 GOBIN 目录下,以供终端使用。
若工作目录为 /Users/wucs/go,需要把 GOPATH 环境变量设置为 /Users/wucs/go,把 GOBIN 环境变量设置为 $GOPATH/bin
Linux/macOS 下,把以下内容添加到 /etc/profile 或者 $HOME/.profile 文件保存即可:
3.项目结构
我们采用 Go Module 模式开进行开发,此模式不必将代码放在 GOPATH 目录中,可以在任意位置来创建项目。
比如项目位置为 \golang\gotour,打开终端,切换到项目目录,然后执行
go mod init example.com/hello
,会生成一个 go.mod 文件。然后在项目根目录创建 main.go 文件。go mod 是 Golang 1.11 版本引入的官方包(package)依赖管理工具,用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。
go mod init “module名字”
初始化模块。go mod tidy
增加缺失的包,移除没用的包
将文章开始的 Hello World 实例写入到 main.go 文件中。
main.go 就是整个项目的入口文件,里面有 mian 函数。
4.编译发布
在项目根目录执行
go build ./main.go
,会在项目根目录生成 main.exe 文件在项目根目录下,终端输入
main
回车,成功打印 “Hello World”,说明程序成功运行。以上生成的可执行文件在项目根目录,也可以把它安装到 $GOBIN 目录或者其他任意位置:
go install /main.go
go install 命令可以将程序生成在 $GOBIN 目录,现在可以在任意位置打开终端,输入 mian 回车,都会打印 “Hello World”。
5.跨平台编译
什么是跨平台编译?比如你在 windows 下开发,可以编译在 linux 上运行的程序。
Go 语言通过两个环境变量来控制跨平台编译,它们分别是 GOOS 和 GOARCH 。
GOOS:代表要编译的目标操作系统,常见的有 Linux、Windows、Darwin 等。
GOARCH:代表要编译的目标处理器架构,常见的有 386、AMD64、ARM64 等
关于 GOOS 和 GOARCH 更多的组合,参考官方文档的 GOARCH 这一节。
Ⅱ、数据类型
1. 都有哪些类型
变量声明
var 变量名 类型 = 表达式
var i int = 10
类型推导
var i = 10
可以根据值的类型来省略变量类型
声明多个变量
类型 int/float64/bool/string 等基础类型都可以被自动推导。
整型
在 Go 语言中,整型分为:
有符号整型:int、int8、int16、int32、int64
无符号整型:uint、uint8、uint16、uint32、uint64
注意:
有符号整型可以表示负数、零、正数,而无符号整型只能为零和正数。
int 和 uint 这两个没有具体的 bit 大小的整型,他们大小可能是 32bit,也可能是 64bit,这个取决于硬件设备 CPU。
在整型中,如果能确定 int 的 bit 就使用明确的 int 类型,这一有助于程序的移植性。
还有一种字节类型 byte,它其实等价于 uint8,可以理解为 uint8 类型的别名,用于定义一个字节,所以字节 byte 类型也属于整型。
浮点数
浮点数就是含有小数的数字,Go 语言中提供了两种精度的浮点数:float32、float64。因为 float64 精度高,浮点计算结果比 float 误差要更小,所以它更被常使用。
布尔型
一个布尔值值只有两种:true 和 false。
定义使用:
var bf bool = false
;使用 bool 关键字定义
字符串
字符串通过类型 string 声明
零值
零值其实就是一个变量的默认值,Go 语言中,如果只声明了一个变量,并没有对其赋值,那么此变量会有一个对应类型的零值。
var b bool // bool 型零值是 false
var s string // string 的零值是""
以下六种类型零值常量都是 nil
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口
2.变量的简短声明
变量名:=表达式在实际项目中,如果能为声明的变量初始化,那么就使用简短的声明方式,这种也是使用最多的。
3.指针
Go 语言中,指针对应的是变量在内存中存储的位置,也就是说指针的值就是遍历的内存地址。通过 & 可以获取变量的地址,也就是指针。*可以获取地址对应的值。
4.常量
常量的值是在编译期就确定好的,确定后不能被修改,可以防止在运行期被恶意篡改。
常量定义
和变量类型,只不过使用关键字 const
const name = "无尘"
在 Go 语言中,只允许布尔型、字符串、数字类型这些基础类型作为常量。
5.iota
iota 是一个常量生成器,可以用来初始化相似规则的常量,避免重复的初始化。
iota 的初始值是 0。
6.字符串
字符串和数字互换
Go 是强类型语言,不同类型的变量是不能相互使用和计算的。不同类型的变量在进行复制或计算时,需要先进行类型转换。
String 包
string 包是 Go SDK 提供的一个标准包。用于处理字符串的工具包。包含查找字符串、拆分字符串、去除字符串的空格、判断字符串是否含有某个前缀或后缀。
更多例子,可以查看 string文档
Ⅲ、控制结构
1. if 条件语句
注意:
if 后的表达无 ‘( )’
每个条件分支中的 ‘{ }’ 是必须的。哪怕只有一行代码。3.if/else 后的 ‘{’ 不能独占一行。否则编译不通过
2. switch 选择语句
if 条件语句比较适合分支比较少的情况。如果有很多分支,switch 会更方便。
注意: Go 语言为防止忘记写 break,case 后自带 break,这和其他语言不一样。
如果确实需要执行下一个 case ,可以使用 fallthrough 关键字
以上结果会输出 1。
当 switch 之后有表达式时,case 后的值就要和这个表达式的结果类型相同,比如这里 j 是 int 类型,所以 case 后就得使用 int 类型。
3. for 循环语句
for 循环由三部分组成,其中需要使用两个 ; 分割:
第一部分是简单语句第二部分是 for 循环的条件第三部分是更新语句这三部分组成都不是必须的,可以被省略。
Go 语言中没有 while 循环,可以通过 for 达到 while 的效果:
Go 中,同样支持 continue,break 控制 for 循环。
continue 跳出本次循环,进入下次循环。
break 强行退出整个循环。
Ⅳ、集合类型
1. Array(数组)
数组存放的是固定长度、相同类型的数据。
1.1 数组声明
var <数组名> = [<长度>]<元素>{元素 1,元素 2}
var arr = [2]int{1,2}
或者
arr := [2]int{1,2}
var <数组名> = [...]<元素类型>{元素 1,元素 2}
var arr = [...]int{1,2}
或者
arr := [...]int{1,2}
var <数组名> = [...]<类型>{索引 1:元素 1,索引 2:元素 2}
var arr = [...]int{1:1,0:2}
或者
arr := [...]int{1:1,0:2}
数组的每个元素在内存中都是连续存放的,每个元素都有一个下标,下标从 0 开始。
数组长度可以省略,会自动根据{}中的元素来进行推导。
没有初始化的索引,默认值是数组类型的零值。
1.2 数组循环
range 表达式返回数组索引赋值给 i,返回数组值赋值给 v。
如果返回的值用不到,可以用 _ 下划线丢弃:
2. 切片
切片和数组类型,可以理解为动态的数组,切片是基于数组实现的,它的底层就是一个数组。对于数组的分割,便可以得到一个切片。
2.1 数组生成切片
slice := array[start:end]
注意:这里包含索引 2,但是不包含索引 5 的元素,即:左闭右开。
经过切片后,切片的索引范围也改变了。array[start:end] 中的 start 和 end 都是可以省略的,start 的默认值是 0 ,end 的默认值为数组的长度。
2.2 切片修改
切片的值也可以被修改,这里也可以证明切片的底层是数组。
修改切片,对应的数组值也被修改了,所以证明基于数组的切片,使用的底层数组还是原来的数组,一旦修改切片的元素值,底层数组对应的值也会被修改。
2.3 切片声明
使用 make 函数声明切片
切片的容量不能比切片长度小。
长度就是元素个数。
容量就是切片的空间。
上面实例在内存上划分了一个容量为 8 的内存空间,但是只是用了 4 个内存空间,剩余的处于空闲状态。当通过 append 往切片追加元素时,会追加到空闲内存上,剩余空间不足时,会进行扩容。
字面量初始化切片
2.3 Append
append 函数对一个切片进行追加元素:
小技巧:
在创建新切片时,最好让长度和容量一样,这样追加操作的时候就会生成新的底层数组,从而和原有数组分离,就不会因为公用底层数组导致修改内容的时候影响多个切片。
2.4 切片循环
切片循环与数组一样,也是使用 for range 方式。
3. Map (映射)
map 是一个无序的 k-v 键值对集合。其中 k 必须是相同类型。k 和 v 的类型可以不同。 k 的类型必须支持 == 比较运算符,这样才可以判断它是否存在,并保证唯一。
3.1 Map 声明初始化
make:
mapName := make(map[string]int)
字面量:
mapName := map[string]int{"无尘":29}
如果不想创建的时候添加键值对,使用空大括号{}即可,切记不能省略。
3.2 Map 获取、删除
获取不存在的 k-v 键值对时,如果 key 不存在,返回的 value 是该值的零值,所以很多时候,需要先判断 map 中的 key 是否存在。
map 的 [] 操作返回两个值
第一个是 value
第二个是标记该 key 是否存在,存在则为 true
delete()函数进行删除
delete(nameAge,"无尘")
delete 有两个参数,一个是 map,一个是要删除的 key 。
4. 遍历 Map
对应 map ,for range 返回两个参数,分别是 k 和 v。
小技巧:for range 遍历 map 的时候,若使用一个返回值,则这个返回值是 map 的 key 。
4.1 Map 的大小
map 不同于切片,map 只有长度,没有容量。可以使用 len 函数获取 map 大小。
5. String 和 []byte
字符串也是一个不可变的字节序列,可以直接转为字节切片 []byte :
string 不止可以直接转为 []byte,还可以使用 [] 操作符获取指定索引的字节值。
字符串是字节序列,每一个索引对应一个字节,在 UTF8 编码下,一个汉字对应三个字节。
如果把一个汉字当做一个长度计算,可以使用 utf8.RuneCountInString 函数。for range 遍历时,是按照 unicode 字符进行循环的,一个汉字占一个长度。
Ⅴ、函数和方法
1. 函数
1.1 函数声明
关键字 func 用于声明一个函数
funcName 函数名
params 函数的参数
result 是函数的返回值,可以返回多个返回值,如果没有可以省略。
body 函数体
示例
1.
a、b 形参类型一致,可以省略其中一个类型的声明
2.多值返回
返回值的部分类型定义需要小括号括起来。
3.命名参数返回
函数中给命名返回参数赋值,相当于函数有了返回值,所以可以忽略 return 后要返回的值了。
4.可变参数
函数的参数是可变的
定义可变参数,只要在参数类型前加三个点 ... 即可
可变参数的类型其实就是切片,下面示例中 params 的参数类型是 []int
1.2 包级函数
函数都会从属于一个包,我们自定义的函数属于 main 包。Println 函数属于 fmt 包。
想要调用其他包内的函数,那么那个函数名称首字母要大写,使其作用域变为公有的。
函数首字母小写,只能在同一个包中被调用
1.3 匿名函数和闭包
匿名函数就是没有名称的函数。
匿名函数可以在函数中进行嵌套,这个匿名函数称为内部函数,内部函数可以使用外部函数的变量,这种方式就是闭包。
由于闭包函数,sum 函数返回一个匿名函数,匿名函数持有外部函数 sum 的变量 i,所以在 main 函数中,每次调用 sum(),i 的值就会 +1。
在 Go 语言中,函数也是一种类型,可以作为函数类型的变量、参数、或者一个函数的返回值。
2. 方法
方法和函数类似,不同之处就是方法必须有一个接收者,这个接收者是一个“类”(类型),这样这个方法就算属于这个“类”。
示例中 String() 就是 Name 这个类型的方法
接收者需要加在 func 和方法名之间,使用()
接收者: (变量,类型)
使用:
3. 值类型接收者、指针类型接收者
方法的接收者可以使用值类型(例如上面示例)或者指针类型。如果接收者是指针,那么对指针的修改是有效的:
注意:在调用方法时,传递的接收者实质上都是副本,只不过一个是值副本,一个是指向这个值的指针的副本。指针指向原有值,所以修改指针指向的值,也就修改了原有值。
方法的调用者,可以是值,也可以是指针((&name).Modify()),Go 语言会自动转义,我们无需关心。
4. 方法表达式
方法可以赋值给变量
无论方法是否有参数,通过方法表达式调用,第一个参数必须是接收者,然后才是方法自身的参数。
Ⅵ、struct 和 interface
1. 结构体
1.1 定义
结构体是种聚合类型,里面可以包含任意类型的值,这些值就是结构体的成员,或成为字段,定义结构体,需要使用 type+struct 关键字组合
type 与 struct 是关键字,用来定义一个新结构体的类型。
person 为结构体名字。
name/age 为结构体的字段名,后面指对应的字段类型。
字段声明和变量类似,变量名在前,类型在后
字段可以是人一个,一个字段都没有的结构体,成为空结构体。
结构体也是一种类型,比如 person 结构体和 person 类型是一个意思。
1.2 声明
像普通字符串、整型医院声明初始化
var p person
声明了一个 person 类型的变量 p,但是没有初始化,所以默认使用结构体里字段的零值。
字面量方式初始化
p := person{"无尘",18}
表示结构体变量 p 的 name 字段初始化为“无尘”,age 字段初始化为 18。顺序必须和字段定义顺序一致。
根据字段名称初始化
p := person{age:18,name:"无尘"}
像这样指出字段名,就可以打乱初始化字段的顺序。也可以只初始化其中部分字段,剩余字段默认使用零值:
p := person{age:30}
1.3 字段结构体
结构体字段可以是任意类型,包括自定义的结构体类型:
对于这样嵌套结构体,初始化和一般结构体类似,根据字段对应的类型初始化即可:
结构体的字段和调用一个类型的方法一样,都是使用点操作符“.”:
2. 接口
2.1 定义
接口是一个抽象的类型,是和调用方的一种约定。接口只需要定义约定,告诉掉用方可以做什么,而不用知道它的内部实现。
接口的定义是 type + interface 关键字类实现。
对应 Stringer 接口,它会告诉调用者可以通过 String()放获取一个字符串,这就是接口的约定,而这个字符串是怎么获取到的,接口并不关心,调用者也不用关心,因为这些是接口的实现者来处理的。
2.2 接口的实现
接口的实现者必须是一个具体的类型:
给结构体类型 person 定义了一个方法,这个方法和接口里的方法名称、参数、返回值都一样,就表示这个结构体 person 实现了 Info 接口。
如果一个接口有多个方法,那么要实现接口中的所有方法才算是实现了这个接口。
2.3 使用
我们先定义一个可以打印 Info 接口的函数:
定义函数 pringInfo,它接收一个 Info 接口类型的参数,然后打印接口 Getinfo 方法返回的字符串。
这个 pringInfo 函数此处是面向接口编程,只有任何一个类型实现了 Info 接口,都可以使用这个函数打印出对应的字符串,而不用关心具体的类型实现。
因为 person 类型实现了 Info 接口,所以变量 p 可以作为函数 printInfo 的参数。
3. 值接受者、指针接受者
实现一个接口,必须实现接口中所有的方法。
定义一个方法,有值类型接收者和指针类型接收者,两者都可以调用方法,因为 Go 编译器自动做了转换。
但是接口的实现,值类型接收者和指针类型接收者不一样
上面接口体 person 实现了 Info 接口,是否结构体指针也实现了该接口呢?
测试发现 p 的指针作为参数函数也是可以正常运行,表明以值类型接收者实现接口,类型本身和该类型的指针类型,都实现了该接口
那么把接收者改成指针类型:
然后再调用函数 printInfo(p)
,代码编译不通过,表明以指针类型接收者实现接口,只有对应的指针类型才被认为实现了接口
当值类型作为接收者,person 类型和*person 类型都实现了该接口。
当指针类型作为接收者,只有 *person 类型实现了该接口。
Ⅶ、错误处理,error 和 panic
1. 错误
在 Go 语言中,错误并不是非常严重,它是可以预期的,可以返回错误给调用者自行处理。
1.1 error 接口
在 Go 语言中,错误是通过内置的 error 接口来表示的,它只有一个 Error 方法来返回错误信息:
这里演示一个错误的示例:
示例故意使用错误的字符串“a”来转为整数,所以这里会打印错误信息:
strconv.Atoi: parsing "a": invalid syntax
一般,error 接口在当函数或方法调用时遇到错误时进行返回,且为第二个返回值,这样调用者就可以根据错误来自行处理。
1.2 error 工厂函数
我们可以使用 errors.New 这个工厂函数来生成错误信息,它接收一个字符串参数,返回一个 error 接口。
当 m 大约 n 的情况下,返回一个错误信息。
1.3 自定义 error
上面工厂函数只能传递一个字符串来返回,要想携带更多信息,这时候可以使用自定义 error:
这里自定义 error,它可以返回更多信息:
上面通过字面量方式创建*testError 来返回。
1.4 error 断言
通过 error 断言来获取返回的错误信息,断言可以将 error 接口转为自己定义的错误类型:
2. Panic 异常
Go 语言是一门静态语言,很多错误可以在编译的时候进行捕获,不过对于数组越界访问、不同类型强制转换这种,会在运行时候才会引起 panic 异常。
我们也可以手动来抛出 panic 异常,这里以连接 mysql 数据库为例:
在以上函数中,如果 ip 地址为空,会抛出 panic 异常。
panic 是 Go 语言内置函数,可以接收 interface{} 类型的参数,也就是说任何类型的值都是可以传递给 panic 函数的:
interface{} 表示空接口,代表任意类型。
panic 是一种非常严重的错误,会使程序中断执行,所以 如果不是影响程序运行的错误,使用 error 即可
2.1 Recover 捕获 Panic 异常
一般我们不对 panic 异常做处理,但是如果有一些需要在程序崩溃前做处理的操作,可以使用内置的 recover 函数来恢复 panic 异常。
程序 panic 异常崩溃的时候,只有 defer 修饰的函数才会被执行,所以 recover 函数要结合 defer 关键字一起使用:
recover 函数捕获了 panic 异常,打印:recover 函数返回的值就是通过 panic 函数传递的参数值。 ip不能为空
recover 函数的返回值就是 panic 函数传递的参数值。
defer 关键字修饰的函数,会在主函数退出前被执行。
Ⅷ、断言和反射
1. 接口断言
提到接口断言,我们先回顾下怎么实现接口?
接口的实现者必须是一个具体类型
类型定义的方法和接口里方法名、参数、返回值都必须一致
若接口有多个方法,那么要实现接口中的所有方法
对于空接口 interface{} ,因为它没有定义任何的函数(方法),所以说 Go 中的所有类型都实现了空接口。
当一个函数的形参是 interface{} 时,意味着这个参数被自动的转为 interface{} 类型,在函数中,如果想得到参数的真实类型,就需要对形参进行断言。
类型断言就是将接口类型的值 x,转换成类型 T,格式为:x.(T)
类型断言 x 必须为接口类型
T 可以是非接口类型,若想断言合法,则 T 必须实现 x 的接口
1.1 语法格式:
示例
断言还有一种形式,就是使用 switch 语句判断接口的类型:
2. 反射
Go 语言提供了一种机制,在运行时可以更新和检查变量的值、调用变量的方法和变量支持的内在操作,但是在编译时并不知道这些变量的具体类型,这种机制被称为反射。
2.1 反射有何用
上面我们提到空接口,它能接收任何东西
但是怎么来判断空接口变量存储的是什么类型呢?上面介绍的类型断言可以实现
如果想获取存储变量的类型信息和值信息就需要使用到反射
反射就是可以动态获取变量类型信息和值信息的机制
2.1 reflect 包
反射是由 reflect 包来提供支持的,它提供两种类型来访问接口变量的内容,即 Type 和 Value。reflect 包提供了两个函数来获取任意对象的 Type 和 Value:
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
示例:
函数 TypeOf 的返回值 reflect.Type 实际上是一个接口,定义了很多方法来获取类型相关的信息:
函数 TypeOf 的返回值 reflect.Value 是一个结构体类型。Value 结构体定义了很多方法,通过这些方法可以直接操作 Value 字段 ptr 所指向的实际数据:
struct 反射示例:
运行结果:
通过反射修改内容
运行结果:
通过反射调用方法
运行结果:
第二部分:Go 的高效并发编程实例
本次给大家介绍的是 go 编程基础,下一节的并发编程后续会推出。感谢大家的观看。
欢迎留言交流,指正
我的 wx: wucs_dd ,公号 《微客鸟窝》,专注于 go 开发技术分享。
版权声明: 本文为 InfoQ 作者【微客鸟窝】的原创文章。
原文链接:【http://xie.infoq.cn/article/32260c99fc7eab822738e0071】。文章转载请联系作者。
评论