go快速入门指南
by 小强,2019-06-13
go语言是目前非常火热的语言,广泛应用于服务器端,云计算,kubernetes容器编排等领域。它是一种开源的编译型程序设计语言,支持并发、垃圾回收机制以提升应用程序性能。它既具有像c这种静态编译型语言的高性能,又具备像python这种动态语言的高效性。很多go程序员都是从C++,Java等面向对象语言因为工作的需要转过来的,因此没有必要从0开始学习go,当初自己的想法是找一篇半小时入门go的博客,貌似没有类似的好文章==。搜到的都是一些从小白入手的臃肿教程,学习起来太慢!!!so,打算写这篇go语言快速入门的指南。
本文写作思路重点在于和C++语言的不同之处入手,强调go的特性,注重快速入门,因此不介绍一些非常基础的知识,非常基础的一些知识可以去看go语言圣经。关于go环境的安装配置以及vscode编辑器的配置在之前的博客已经介绍,请移步。本文整体组织结构如下:
go程序开发的一般结构
基本数据类型
变量的声明和赋值
运算符和指针
判断语句if和循环语句for
数组、切片、map
函数function
方法、接口、反射
并发
1.go程序开发的一般结构
在学习任何一门语言的时候,首先都是给出hello world的示例,因此本文也不免俗,看看第一个go语言程序:
/1.1 template.go/
//当前程序的包名
package main
//导入其他的包
import (
"fmt"
)//由main函数作为函数入口
func main () {
fmt.Println("Hello World!")
}
和python语言很像,go程序都必须包含在一个包package中,go程序一般由三部分组成:包声明部分、第三方包导入部分和函数声明部分。go语言使用关键字package声明要创建的包;使用import导入第三方包;使用关键字func声明要创建的函数。
按照惯例,处于同一个文件里的代码文件,必须使用同一个包名,包和文件夹的名字相同。Go编译器不允许声明导入某个包却不使用。使用下划线可以让编译器接收这类导入,并且调用对应包内的所有代码文件中定义的init函数。 init函数会在main函数执行前执行。
1.1 编写go程序步骤
初学者按照以下步骤编写go程序:
1)在工作目录(比如D:\go\development)的src文件夹中创建源文件helloworld.go;.
2)直接将helloworld.go拖入vscode进行编辑;
3)在vscode的终端输入go run helloworld.go,程序就会输出hello world!
1.2 go的语法要点
go语言语句结束不使用分号,直接另起一行即可
go语言规定,函数、控制结构(if,for,switch)等的左大括号“{”必须和函数声明或控制结构放在同一行
可见性规则:go语言使用大小写来决定常量、变量、类型、接口、结构或函数是否可以被外部包所调用。
函数名首字母小写,即为private。
函数名首字母大写,即为public。
2 .基本数据类型
布尔型bool长度为1字节,取值范围true,false,不同点:注意不能使用数字0/1来代表true或者false。
整型:int/uint,根据运行平台可能是32或者64
8为整型:int8/uint8,长度是1字节,取值范围:-128~127/0-255
计算方法,2^8 / 2给负数部分,剩下分一个给0,最后的部分给整数部分。
int16/uint16,int32/uint32,int64/uint64
浮点型:float32/float64
长度:4/8字节,小数位,精确到7/15小数位
注意go没有double类型。
复数:complex64/complex128
长度:8/16字节
足够保存指针的32为或64为整数型:uintptr
其他值类型:array,struct,string
引用类型:slice(切片,特有类型),map(哈希表),chan(通道)
接口类型:interface
函数类型:func
类型的零值:就是默认值,int默认是0,bool默认是false,字符串默认是空字符串。
类型别名方法格式:
//type 别名 基本类型
type byte int8
3.变量的声明和赋值
全局变量不能省略var关键字,函数内的变量声明可以省略。 go语言中使用关键字func声明函数,关键字后面跟函数名、参数名以及返回值。
全局变量的声明可以使用var()的方式进行简写
全局变量的声明不可以省略var,但是可使用并行方式
所有变量都可以使用类型推断
局部变量不可以使用var()的方式简写,只能使用并行方式。
3.1 变量声明
go语言使用关键字var来声明变量,声明的一般格式如下所示:
var 【varibleType】
var count int
在声明变量的同时可以使用=给变量赋初值:
var count int = 10
其中变量类型int也可以省略,编译器会依据赋的初值自动推断变量类型:
var count = 10
在声明变量的同时还允许省略掉关键字“var”,使用":"取代。
count := 10
3.2 常量的声明
常量的声明格式如下所示:
const 【constType】 =
常量的值在编译时就已经确定
常量的定义格式与变量基本相同
等号右侧必须是常量或者常量表达式
常量表达式中的函数必须是内置函数
在定义常量组是,如果不提供初始值,则表示将使用上行的表达式
使用相同的表达式不代表具有相同的值
iota是常量的计数器,从0开始,组中每定义一个常量自动递增1
通过初始化规则与iota可以达到枚举的效果
每遇到一个const关键字,iota就会重置为0
注意常量的定义必须是大写字母。但是如果是大写字母的话,就会变成public变量,为了不被包外部使用,一般在前面加或者c。
const a = 1
const (
b, c = 2,3
)
const d,f = 4,5
const (
a = iota //0
b = iota //1
)
4.运算符和指针
4.1 运算符
go的运算符均是从左至右结合。
优先级(从高到底)
^ !(一元运算符)
/ % [ ] & &^(二元运算符)
== != < = >
<- (专门用于channel)
&& (好处是运行第一个不满足要求就不在执行接下来的表达式)
||
其中位运算符介绍:
实际例子:
6: 0110
11:1011
-------------
&: 0010
|: 1111
^: 1101
&^:0100
& 与:都是1的时候结果才能是1
| 或:只要有一个是1就是1
^ 两个只能有一个是1才能是1
&^第二个计算数是1的时候将第一个计算数对应位置为0,如果第二个计算数该位是0的话,对应第一个计算数的位不变。
4.2 指针
Go虽然保留了指针,但不同点在于go不支持指针运算以及->运算符,而直接采用.选择符来操作指针目标对象的成员。
操作符&去变量地址,使用通过指针间接访问目标对象
默认值为nil而非NULL
递增递减语句
在go当中,++ 与--是作为语句而不是作为表达式。必须单独作为一行,只能A++这种形式。
A++//只能作为单独的语句放在一行,且只能++放在右边
x, y := 1, 2
var p = 【2】int{&x, &y}
fmt.Println(p【0】)
var arr = 【2】int{x, y}
pf := &arr
fmt.Println(pf)
其实就是将【】*int看成一个类型,后面的{}就是初始化操作。
5.判断语句if和循环语句for
5.1 判断语句if
条件表达式没有括号
支持一个初始化表达式(可以是并行方式)
左大括号必须和条件语句或else在同一行
支持单行模式
初始化语句中的变量为block级别,同时隐藏外部同名变量
if a > 1 {
fmt.Println(a)
}
if b :=1;b > 1 { }
注意else必须和if的右括号}在同一行才行,不然出错。
if a {
}else {}
if后面定义的变量,属于if语句的局部变量,只能在对应的if-else中使用,不能在外部使用。之间通过;分割语句。
5.2 循环语句for
go只有for一个循环语句关键字,但支持3种形式
初始化和步进表达式可以是多个值
条件语句每次循环都会被重新检查,因此不建议在条件语句中使用函数,尽量提前计算好条件并以变量或常量代替。
左大括号必须和条件语句在同一行
1. for init; condition; post {} // 和c语言的for一样
2. for condition {} //while
3. for {} //for(;;)
init: 一般为赋值表达式,给控制变量赋初值;一定在这里赋初值,不然出错
condition: 关系表达式或逻辑表达式,循环控制条件;
post: 一般为赋值表达式,给控制变量增量或减量。
for语句执行过程如下:
1)先对表达式1赋初值;
2)判别赋值表达式init是否满足给定条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行post,进入第二次循环,再判别condition;否则判断condition的值为假,不满足条件,就终止for循环,执行循环体外语句。
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {
newMap【key】 = value
}
关键字range会返回两个值,第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。而不是返回对该元素的引用。
switch语句
switch语句默认情况下case最后自带break语句,匹配成功后就不会执行其他case,如果我们需要执行后面的case,可以使用fallthrough。
其中var1可以是任何类型,val1和val2可以是同类型的任意值,类型不被局限于常量或证书,但必须是相同的类型,或者最终结果为相同类型的表达式。可以同时测试多个可能符合条件的值,使用逗号分隔它们。例如 case val1,val2,val3。
switch var1 {
case val1:
...
case val2:
...
default:
...
}
跳转语句goto,break,continue
三个语法都可以配合标签使用。
标签名区分大小写,若不适用会造成编译错误。
Break与continue配合标签可用于多层循环的跳出。
Goto是调整执行位置,与其他2个语句配合标签的结果并不相同。 标签值是包含接下来的一个语句,continue是退出这个标签的值。
6 .数组、切片、map
6.1 数组
定义数组的格式: var 【n】 ,n >= 0(表示数组的元素个数)。
var a 【2】int
var b 【1】int
记住ab是不同的类型,不能直接赋值,元素个数也是数组类型的一种。需要使用循环语句进行操作。 也可以不指定数组元素的个数。
a := 【...】int{1,2,3} //元//代码效果参考:http://hnjlyzjd.com/hw/wz_25368.html
素个数为3个a := 【...】int{0:1,1:2}//位置0赋值为1,位置1赋值为2
a := new(【10】int)
数组长度也是类型的一部分,因此具有不同长度的数组为不同类型。
注意区分指向数组的指针和指针数组
数组在go中为值类型,不是引用类型,会全部拷贝值
数组之间可以使用==或!=进行比较,但不可以使用
可以使用new来创建数组,此方法返回一个指向数组的指针
go支持多维数组。
需要注意的是所有值类型变量在赋值或作为参数传递的时候将产生一次复制操作。如果将数组作为函数的参数类型,则在函数调用是将该参数将发生数据复制,函数体内无法修改数组的内容,因为函数体内操作是变量的一个副本。
多维数组的声明如下所示,其中第一维度行的数量是可以省略的,使用...代替。
arr := 【2】【3】int{
{1, 2, 3},
{2, 3, 4}}
表示2个元素,每个元素是一维数组,有三个元素。
6.2 切片slice
切片是数组的一个引用,它会生成一个指向数组的指针,并通过切片长度关联到底层数组部分或者全部元素,还提供了一系列对数组的管理功能(append,copy),可以随时动态的扩充存储空间。属于变长数组,相当于C++的vector。创建切片的格式如下:
var sliceName 【】dataType
创建切片时,不需要指定切片的长度。下面是一个具体的例子。
var slice1 【】int
6.2.1 初始化方法
1)如果引用底层数组的元素,初始化方法如下:
slice1 = array【start : end】
//以下是三种方式
slice1 = array1
slice1 = array1【 : 】
slice1 = array【0 : len(array1)】
2)直接创建切片
//代码效果参考:http://hnjlyzjd.com/xl/wz_25366.html
即在定义的同时初始化切片元素,如下例:var slice1 = 【】int{1,2,3,4,5}
3)使用make函数创建切片
下式表示,创建整型切片slice1,元素个数为5,元素初值为0,并预留10个元素的存储空间。
var slice1 = make(【】int, 5, 10)
对切片的访问方式可以通过下标的方式访问,也可以通过range关键字进行访问,同数组。
其本身并不是数组,它指向底层的数组,使用【】来做声明
作为变长数组的替代方案,可以关联底层数组的局部或全部
属于引用类型
可以直接创建或从底层数组获取生成
使用len()获取元素个数,cap()获取容量
一般使用make()创建
如果多个slice指向相同底层数组,其中一个的值改变会影响全部
make(【】T,len,cap)
其中cap可以省略,则和len的值相同
len表示存数的元素个数,cap表示容量
//从数组初始化
var arr = 【...】int{1,2,3}
var slice_a 【】int
slice_a = arr【1:2】//下标位置,【1,2),包括首位置,不包含末尾的2位置
6.2.2 切片的操作
- Reslice:
Reslice时索引以被slice的切片为准
索引不可以超过被slice的切片容量的cap()值
索引越界不会导致底层数组的重新分配而是引发错误
- Append
可以在slice尾部追加元素
可以将一个slice追加在另一个slice尾部
如果最终长度未超过追到到slice的容量则返回原始slice
如果超过追加到的slice的容量则将重新分配数组并拷贝原始数据
使用...运算符将一个切片的所有元素追加到另一个切片中
append(s1,s2...)
- copy
copy(s1,s2),必须保证s1有足够的空间来存储s2的值。
- 多维切片
slice := 【】【】int{ {1, 2}, {3, 4}}
使用切片做值函数传递时,以值的形式传递切片,由于切片的尺寸很小,所以成本很低,与切片关联的数据保存在底层数组中,不属于切片本身,所以切片的效率很高。slice的拷贝可以使用 s2 := s1【:】,拷贝首元素省略,拷贝末尾元素也可以省略,:表示拷贝全部元素。
6.3 map
map就是理解为C++里面的map,是key-value类型,也称为字典或者哈希表。
6.3.1 声明格式
var mapName map【keyType】 valueType
var map1 map【string】 int
在该例中,声明了一个键值类型为字符串,值类型为整型的字典map1。
6.3.2 字典的初始化和创建
使用“{ }”操作符对字典进行初始化操作,或者使用make()函数来创建字典。初始化或者创建后,就可以使用“=”操作符对字典动态的增添数据项了。
var map1 map【string】 int {}
map1【"key1"】 = 1
也可以使用下面的方式进行创建:
var map1 map【string】 int
map1 = make(map【string】 int)
map1【"key1"】 = 1
6.3.3 map的访问和操作
map通过key来访问value,访问格式如下所示:
Value = mapName【Key】
map的查找:如果查找的key存在,则将key对应的value值赋予v,OK为true,反之,如果Key不存在,则v等于0,OK为false。
v,OK := mapName【Key】
map的删除:
delete()用于删除容器内的元素,也可以用于删除map内的键值对,例如:
下面将从map1中删除键值为key1的键值对,如果key1这个键不存在,那么这个调用将什么也不会发生。
delete(map1,“key1”)
类似其他语言中的哈希表或者字典,以key-value形式存储数据
key必须是支持==或!=比较运算的类型,不可以是函数、map或者slice
map查找比线性搜索快很多,但比使用索引访问数据的类型慢100倍
map使用make()创建,支持:=这种简写方式。
make(【keyType】valueType,cap),cap表示容量,可省略
超出容量时会自动扩容,但尽量提供一个合理的初始值
使用len()获取元素个数
键值对不存在时自动添加,使用delete()删除某键值对
使用for range对map和slice进行迭代操作
记住每个map都必须进行单独的初始化操作。 使用make进行初始化操作。有几层map就需要使用几次make进行初始化操作。
map的迭代操作
for k,v := range m {}
7.函数function
go函数不支持嵌套,重载和默认参数
但支持以下特性
无需声明原型 、不定长度变参、多返回值、命名返回值参数
匿名函数、闭包
定义函数使用关键字func,且左大括号不能另起一行
函数也可以作为一种类型使用
函数声明的基本结构如下:
func functionName(参数列表) 返回值 {
functionBody
.
.
.
return 语句
}
不定长变参的使用
不定长变参使用...表示,要放在所有的参数最后面,传入的a变为一个slice
func A (a ...int) {}
闭包closure
闭包就是在一个函数中声明一个匿名函数,然后返回这个匿名函数。
func f(i int) func() int {
return func() int {
i++
return i
}
}
defer
执行方式类似其他语言的析构函数,在函数体执行结束之后按照调用顺序的相反顺序逐个执行
即使函数发生严重错误也会执行
支持匿名函数的调用
常用于资源清理、文件关闭、解锁以及记录时间等操作
通过与匿名函数配合可在return之后修改函数计算结果
如果函数体内某个变量作为defer时匿名函数的参数,则在定义defer时即已经获得了拷贝,否则则是引用某个变量的地址。
go没有异常机制,但有panic/recover模式来处理错误
panic可以在任何地方引发,但recover只有在defer调用的函数中有效
defer,panic,recover
go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
func B() {
defer func() {
if err := recover(); err != nil {
fmt.Println("Recover in B")
}
}()
panic("Panic in B")
}
代码分析
func main() {
var fs = 【4】func(){}
for i := 0; i < 4; i++ {
defer fmt.Println("defer i = ", i)
defer func() {
fmt.Println("defer_closure i = ", i)
}()
fs【i】 = func() { fmt.Println(<span style="color: rgba(12