GO 基本介绍
GO 基本概念
GO 语言特性
GO 程序的基本结构
GO 标识符 关键字 变量 常量
GO 数据类型
GO 操作符 与 日期
GO 流程控制
GO 函数介绍
GO 闭包的介绍
Go 语言诞生于谷歌,由计算机领域的三位宗师级大牛 Rob Pike、Ken Thompson 和 Robert Griesemer 写成。由于出身名门,Go 在诞生之初就吸引了大批开发者的关注。诞生十年以来,已经涌出了很多基于 Go 的应用。就在不多久之前,知乎也舍弃了 Python,转 用Go 重构推荐系统 。
谷歌前员工 Jake Wilson 认为,比起大家熟悉的 Python,Go 语言其实有很多优良特性,很多时候都可以代替 Python,他已经在很多任务中使用 Go 语言替代了 Python。那么 Go 语言到底有着什么样的独特魅力 ?让我们一起来了解一下吧!
对于一门只有十年历史的新语言,Go 的发展势头相当迅猛,容器界的扛把子 Docker 就是用 Go 写的,国内也有不少团队广泛使用 Go。近日,HackerRank 在社区发起了程序员技能调查,来自 100 多个国家、超过 70000 名开发者参与其中。调查结果显示,2019 年,程序员最想学习的编程语言 Top 3 分别是 Go、Kotlin 和 Python,其中 Go 以 37.2% 的比例排在首位。
但 Go 要想撼动编程界的常青树 Java 二十多年的地位无疑难度颇大。据 HackerRank 数据显示,2018 年,Java 在开发者最受欢迎的编程语言排行榜中仍然排名第 2,Python 排名第 4,Go 排名第 13,距离第一名 JavaScript 还有不小的差距。
Golang 目录结构划分
a. 个人项目
b. 公司项目
包的概念
a. 和python⼀样,把相同功能的代码放到⼀个⽬录,称之为包
b. 包可以被其他包引⽤
c. main包是⽤来⽣成可执⾏⽂件,每个程序只有⼀个main包
d. 包的主要⽤途是提⾼代码的可复⽤性
基本命令的介绍
a. go run 快速执⾏go⽂件,就像执⾏脚本⼀样
b. go build 编译程序,⽣成⼆进制可执⾏⽂件
c. go install 安装可执⾏⽂件到bin⽬录
d. go test 执⾏单元测试或压⼒测试
e. go env 显示go相关的环境变量
f. go fmt 格式化源代码
GO 程序的结构
a. go源码按package进⾏组织,并且package要放到⾮注释的第⼀⾏
b. ⼀个可执⾏程序只有⼀个main包和⼀个main函数
c. main函数是程序的执⾏⼊⼝
垃圾回收
a. 内存⾃动回收,再也不需要开发⼈员管理内存
b. 开发⼈员专注业务实现,降低了⼼智负担
c. 只需要new分配内存,不需要释放,golang语⾔特性
天然并发
a. 从语⾔层⾯⽀持并发,⾮常简单。只需要go⼀下
b. goroutine,轻量级线程,创建成千上万个goroute成为可能
func calc() { //⼤量计算 }
func main() { go calc() }
channel
a. 管道,类似unix/linux中的pipe
b. 多个goroute之间通过channel进⾏通信
c. ⽀持任何类型
多返回值
a. ⼀个函数返回多个值
编译型语⾔
a. 性能只⽐C语⾔差10%
b. 开发效率和python、php差不多
a. 任何⼀个代码⽂件⾪属于⼀个包
package main
import “fmt”
func main() {
fmt.Println(“hello, world”)
}
b. import 关键字,引⽤其他包
c. 开发可执行程序,package main, 并且有且只有⼀个main⼊⼝函数
d. 包中函数调⽤
同一个包中函数,直接用函数名调, 不同包中函数,通过包名+点+ 函数名进⾏调⽤
e. 包访问控制规则
大写意味着这个函数/变量是可导出,小写意味着这个函数/变量是私有的,包外部不能访问
标识符
标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列,标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。通俗的讲就是凡可以自己定义的名称都可以叫做标识符。
下划线_是一个特殊的标识符,称为空白标识符,它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用_作为变量对其它变量进行赋值或运算。
在使用标识符之前必须进行声明,声明一个标识符就是将这个标识符与常量、类型、变量、函数或者代码包绑定在一起。在同一个代码块内标识符的名称不能重复。
标识符的命名需要遵守以下规则:
a. 由 26 个英文字母、0~9、_组成;
b. 不能以数字开头,例如 var 1num int 是错误的;
c. Go语言中严格区分大小写;
d. 标识符不能包含空格;
d. 不能以系统保留关键字作为标识符,比如 break,if 等等。
命名标识符时还需要注意以下几点:
a. 标识符的命名要尽量采取简短且有意义;
b. 不能和标准库中的包名重复;
c. 为变量、函数、常量命名时采用驼峰命名法,例如 stuName、getVal;
当然Go语言中的变量、函数、常量名称的首字母也可以大写,如果首字母大写,则表示它可以被其它的包访问(类似于 Java 中的 public);如果首字母小写,则表示它只能在本包中使用 (类似于 Java 中 private)。
在Go语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:
预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。
了解规则后,判断下面标识符哪个是错误的:
A. 88ab(错) B. _ab28 C. ab_28
关键字
关键字是Go语⾔预先定义好的,有特殊含义的标识符。共25 个。
变量
语法:var identifier type
var a int
var b string
var c bool
var d int = 8
var e string = “hello”
或者是:
var (
a int //0
b string //“”
c bool //false
d int = 8 // 8
e string = “hello” //hello
)
常量
常量使⽤const 修饰,代表永远是只读的,不能修改。语法:const identifier [type] = value,其中type可以省略。
举例: const b string = “hello world”
const b = “hello world”
const Pi = 3.1414926
const a = 9/3
更加优雅的写法:
const(
a = 1
b = 2
c = 3
)
更加专业的写法:
const (
a = iota
b
c
)
const(
a = 1 << iota
b
c
)
数据类型
a. 布尔类型
a. var b bool 和 var b bool = true 和 var b = false ,默认false
b. 操作符 == 和 != 相等性判断
c. 取反操作符:!b 数据类型和操作符
d. && 和 || 操作符 与或
e. 格式化输出占位符: %t fmt.Println("%t %t\n",a, b)
golang fmt包 Printf方法详解
General
%v 以默认的方式打印变量的值
%T 打印变量的类型
Integer
%+d 带符号的整型,fmt.Printf("%+d", 255)输出+255
%q 打印单引号
%o 不带零的八进制
%#o 带零的八进制
%x 小写的十六进制
%X 大写的十六进制
%#x 带0x的十六进制
%U 打印Unicode字符
%#U 打印带字符的Unicode
%b 打印整型的二进制
Float
%f (=%.6f) 6位小数点
%e (=%.6e) 6位小数点(科学计数法)
%g 用最少的数字来表示
%.3g 最多3位数字来表示
%.3f 最多3位小数来表示
String
%s 正常输出字符串
%q 字符串带双引号,字符串中的引号带转义符
%#q 字符串带反引号,如果字符串内有反引号,就用双引号代替
%x 将字符串转换为小写的16进制格式
%X 将字符串转换为大写的16进制格式
% x 带空格的16进制格式
String Width (以5做例子)
%5s 最小宽度为5
%-5s 最小宽度为5(左对齐)
%.5s 最大宽度为5
%5.7s 最小宽度为5,最大宽度为7
%-5.7s 最小宽度为5,最大宽度为7(左对齐)
%5.3s 如果宽度大于3,则截断
%05s 如果宽度小于5,就会在字符串前面补零
Struct
%v 正常打印。比如:{sam {12345 67890}}
%+v 带字段名称。比如:{name:sam phone:{mobile:12345 office:67890}
%#v 用Go的语法打印。
比如main.People{name:”sam”, phone:main.Phone{mobile:”12345”, office:”67890”}}
Boolean
%t 打印true或false
Pointer
%p 带0x的指针
%#p 不带0x的指针
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于"U+%04X"
b. 整形和浮点数类型
整形可以分为10个类型:
int 和 uint 的区别就在于一个 u,有 u 说明是无符号,没有 u 代表有符号。解释这个符号的区别
以 int8 和 uint8 举例,8 代表 8个bit,能表示的数值个数有 2^8 = 256。
uint8 是无符号,能表示的都是正数,0-255,刚好256个数。
int8 是有符号,既可以正数,也可以负数,那怎么办?对半分呗,-128-127,也刚好 256个数。
int8 int16 int32 int64 这几个类型的最后都有一个数值,这表明了它们能表示的数值个数是固定的。
而 int 没有并没有指定它的位数,说明它的大小,是可以变化的,那根据什么变化呢?
当你在32位的系统下,int 和 uint 都占用 4个字节,也就是32位。
若你在64位的系统下,int 和 uint 都占用 8个字节,也就是64位。
出于这个原因,在某些场景下,你应当避免使用 int 和 uint ,而使用更加精确的 int32 和 int64,比如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)
不同进制的表示方法
出于习惯,在初始化数据类型为整形的变量时,我们会使用10进制的表示法,因为它最直观,比如这样,表示整数10.
var num int = 10
不过,你要清楚,你一样可以使用其他进制来表示一个整数,这里以比较常用的2进制、8进制和16进制举例
2进制:以0b或0B为前缀
var num01 int = 0b1100
8进制:以0o或者 0O为前缀
var num02 int = 0o14
16进制:以0x 为前缀
var num03 int = 0xC
下面用一段代码分别使用二进制、8进制、16进制来表示 10 进制的数值:12
package main
import (
"fmt"
)
func main() {
var num01 int = 0b1100
var num02 int = 0o14
var num03 int = 0xC
fmt.Printf("2进制数 %b 表示的是: %d \n", num01, num01)
fmt.Printf("8进制数 %o 表示的是: %d \n", num02, num02)
fmt.Printf("16进制数 %X 表示的是: %d \n", num03, num03)
}
输出如下
2进制数 1100 表示的是: 12
8进制数 14 表示的是: 12
16进制数 C 表示的是: 12
以上代码用过了 fmt 包的格式化功能,你可以参考这里去看上面的代码
浮点型
浮点数类型的值一般由整数部分、小数点“.”和小数部分组成。
其中,整数部分和小数部分均由10进制表示法表示。不过还有另一种表示方法。那就是在其中加入指数部分。指数部分由“E”或“e”以及一个带正负号的10进制数组成。比如,3.7E-2表示浮点数0.037。又比如,3.7E+1表示浮点数37。
有时候,浮点数类型值的表示也可以被简化。比如,37.0可以被简化为37。又比如,0.037可以被简化为.037。
有一点需要注意,在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。比如,03.7表示的一定是浮点数3.7。
float32 和 float64
Go语言中提供了两种精度的浮点数 float32 和 float64。
float32,也即我们常说的单精度,存储占用4个字节,也即4*8=32位,其中1位用来符号,8位用来指数,剩下的23位表示尾数
float64,也即我们熟悉的双精度,存储占用8个字节,也即8*8=64位,其中1位用来符号,11位用来指数,剩下的52位表示尾数
那么精度是什么意思?有效位有多少位?
精度主要取决于尾数部分的位数。
对于 float32(单精度)来说,表示尾数的为23位,除去全部为0的情况以外,最小为2^-23,约等于1.19*10^-7,所以float小数部分只能精确到后面6位,加上小数点前的一位,即有效数字为7位。
同理 float64(单精度)的尾数部分为 52位,最小为2^-52,约为2.22*10^-16,所以精确到小数点后15位,加上小数点前的一位,有效位数为16位。
通过以上,可以总结出以下几点:
float32 和 float64 可以表示的数值很多
浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:
常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;
常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;
float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324。
数值很大但精度有限
人家虽然能表示的数值很大,但精度位却没有那么大。
float32的精度只能提供大约6个十进制数(表示后科学计数法后,小数点后6位)的精度
float64的精度能提供大约15个十进制数(表示后科学计数法后,小数点后15位)的精度
这里的精度是什么意思呢?
比如 10000018这个数,用 float32 的类型来表示的话,由于其有效位是7位,将10000018 表示成科学计数法,就是 1.0000018 * 10^7,能精确到小数点后面6位。
此时用科学计数法表示后,小数点后有7位,刚刚满足我们的精度要求,意思是什么呢?此时你对这个数进行+1或者-1等数学运算,都能保证计算结果是精确的
import "fmt"
var myfloat float32 = 10000018
func main() {
fmt.Println("myfloat: ", myfloat)
fmt.Println("myfloat: ", myfloat+1)
}
输出如下
myfloat: 1.0000018e+07
myfloat: 1.0000019e+07
上面举了一个刚好满足精度要求数据的临界情况,为了做对比,下面也举一个刚好不满足精度要求的例子。只要给这个数值多加一位数就行了。
换成 100000187,同样使用 float32类型,表示成科学计数法,由于精度有限,表示的时候小数点后面7位是准确的,但若是对其进行数学运算,由于第八位无法表示,所以运算后第七位的值,就会变得不精确。
这里我们写个代码来验证一下,按照我们的理解下面 myfloat01 = 100000182 ,对其+5 操作后,应该等于 myfloat02 = 100000187,
import "fmt"
var myfloat01 float32 = 100000182
var myfloat02 float32 = 100000187
func main() {
fmt.Println("myfloat: ", myfloat01)
fmt.Println("myfloat: ", myfloat01+5)
fmt.Println(myfloat02 == myfloat01+5)
}
但是由于其类型是 float32,精度不足,导致最后比较的结果是不相等(从小数点后第七位开始不精确)
myfloat: 1.00000184e+08
myfloat: 1.0000019e+08
false
由于精度的问题,就会出现这种很怪异的现象,myfloat == myfloat +1 会返回 true.
c. 字符串类型
var str string
var str string = “hello world”
字符串串输出占位符 %s
万能输出占位符: %v
字符串的两种表示方式
双引号, “”,可以包含控制字符
反引号, ``,所有字符都是原样输出
字符串常⽤用操作
⻓度:len(str)
拼接:+,fmt.Sprintf
分割:strings.Split
包含: strings.Contains
前缀或后缀判断:strings.HasPrefix, strings.HasSuffix
⼦串出现的位置: strings.Index(), strings.LastIndex()
join操作: strings.Join(a[]string, sep string)
字符串原理解析
字符串底层就是一个byte数组,所以可以和[]byte类型互相转换
字符串之中的字符是不能修改的,那怎么修改呢 转成字符切片
字符串是由byte字节组成,所以字符串的⻓度是byte字节的长度
rune类型⽤来表示utf8字符,⼀个rune字符由1个或多个byte组成
操作符
a. 算术运算符
+:相加;
-:相减;
*:相乘;
/:相除;
%:求余;
++:自增;
--:自减;
其中,++ 与 -- 不能用于赋值表达式, 如: count2 := count++;并且在 Go 语言中,不存在如:++count 表达式。
b. 关系运算符
==:检查两个值是否相等,如果相等返回 true,否则返回 false;
!=:检查两个值是否不相等,如果不相等返回 true,否则返回 false;
>:检查左边值是否大于右边值,如果是返回 true,否则返回 false;
<:检查左边值是否小于右边值,如果是返回 true,否则返回 false;
>=:检查左边值是否大于等于右边值,如果是返回 true,否则返回 false;
<=:检查左边值是否小于等于右边值,如果是返回 true,否则返回 false;
c. 逻辑运算符
&&:逻辑 AND 运算符。如果两边的操作数都是 true,则条件为 true,否则为 false;
||:逻辑 OR 运算符。如果两边的操作数有一个 true,则条件为 true,否则为 false;
!:逻辑 NOT 运算符。如果条件为 true,则逻辑 NOT 添加为 true,否则为 false;
位运算符
位运算符是对整数在内存中的二进制进行操作。
&:按位与运算符。其功能是参与运算的两个数的二进制按位对齐,当对应位都为 1 时,才返回 1;
fmt.Println(3 & 4) // 0
// 计算过程
// 0011 => 3 的二进制
// 0100 => 4 的二进制
// &
// ---------------------------
// 0000 => 0 的二进制
|:按位或运算符。其功能是参与运算的两个数的二进制按位对齐,当对应位中只要有一位是 1,就返回 1;
fmt.Println(3 | 4) // 7
// 计算过程
// 0011 => 3 的二进制
// 0100 => 4 的二进制
// &
// ---------------------------
// 0111 => 7 的二进制
^:按位异或运算符。其是参与运算的两个数的二进制按位对齐,当对应位有一位是 1,就返回 1;如果对应两位都是 1 或 0,就返回 0;
fmt.Println(25 ^ 3) // 26
// 计算过程
// 0001 1001 => 25 的二进制
// 0000 0011 => 3 的二进制
// ^
// ---------------------------
// 0001 1010 => 26 的二进制
<<:左移运算符。其功能是将数值的二进制所有位向左移动指定的位数;
fmt.Println(3 << 3) // 24
// 计算过程
// 0000 0011 => 3 的二进制
// 3
// <<
// ---------------------------
// 0001 1000 => 24 的二进制
>>:右移运算符。其功能是将数值的二进制所有位向右移动指定的位数;
fmt.Println(3 >> 3) // 0
// 计算过程
// 0000 0011 => 3 的二进制
// 3
// >>
// ---------------------------
// 0000 0000 => 0 的二进制
d. 赋值运算符
=:简单的赋值运算符,将一个表达式的值赋给一个左值;
+=:相加后再赋值;
-=:相减后再赋值;
*=:相乘后再赋值;
/=:相除后再赋值;
%=:取余后再赋值;
&=:按位与后赋值;
|=:按位或后赋值;
^=:按位异或后赋值;
<<=:左位移后赋值;
>>=:右位移后赋值;
时间与日期类型
time包
time.Time类型,⽤来表示时间
获取当前时间, now := time.Now()
time.Now().Day(),time.Now().Minute(),time.Now().Month(),time.Now().Year()
格式化,fmt.Printf(“%02d/%02d%02d %02d:%02d:%02d”, now.Year()...)
获取当前时间戳,time.Now().Unix()。
时间戳转Time类型。
time.Duration用来表示纳秒
一些常量
a. time.Nanosecond 纳秒 ns
b. time.Microsecond 微秒 µs 1000* Nanosecond
c. time.Millisecond 毫秒 ms 1000* Microsecond
d. time.Second 秒 1000*Millisecond
格式化
a. now:=time.Now()
b. fmt.Println(now.Format("02/1/2006 15:04"))
c. fmt.Println(now.Format("2006/1/02 15:04"))
d. fmt.Println(now.Format("2006/1/02"))
流程控制
if condition {
//do something
} else if condition {
//do something
} else {
//do something
}
或者:
if statement; condition {
}
例子:
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")
}
}
a. 循环
for initialisation; condition; post {
}
例子:
package main
import ( "fmt")
func main() {
for i:=1; i<= 10;i ++ {
fmt.Printf(" %d",i)
}
}
b. break,终⽌循环
package main
import ( "fmt")
func main() {
for i:=1; i<=10;i++ {
if i > 5 {
break
}
fmt.Printf("%d",i)
}
fmt.Printf("\nline after for loop")
}
12345 "\nline after for loop"
c. continue,终⽌本次循环
package main
import ( "fmt" )
func main() {
for i :=1;i<=10;i++ {
if i%2 == 0 {
continue
}
fmt.Printf("%d", i)
}
}
13579
package main
import (fmt')
func main() {
for no, i:=10,1; i<=10 && no <=19; i, no = i+1,no+1 {
fmt.Printf("%d * %d = %d\n", no, i, no*i)
}
}
d. switch 语句
package main
import ("fmt")
func main() {
finger:=4
switch finger {
case 1:
fmt.Println("Thumb")
case 2:
fmt.Println("Index")
case 3:
fmt.Println("Middle")
case 4:
fmt.Println("Ring")
case 5:
fmt.Println("Pinky")
}
}
a. 定义:有输⼊、有输出,⽤来执行⼀个指定任务的代码块。
func functionname([parametername type]) [returntype] {
//function body
}
//其中参数列列表和返回值列列表是可选
例如:
func add(a int, b int) int {
Return a + b
}
无参数和返回值的函数
func functionname() {
//function body
}
如果连续的一系列参数的类型是一样,前面的类型可以不写,例如:
func add(a, b int) int {
Return a + b
}
函数调用
func add(a, b int) int {
Return a + b
}
func main() {
sum := add(2, 3)
}
多返回值
func calc(a, b int) (int, int) {
sum := a + b
sub := a - b
return sum, sub
}
func main() {
sum, sub := add(2, 3)
}
对返回值进行命名
func calc(a, b int) (sum int, sub int) {
sum = a + b
sub = a - b
return
}
func main() {
sum, sub := add(2, 3)
}
_标识符 这样就会忽略sub的值
func calc(a, b int) (sum int, sub int) {
sum = a + b
sub = a - b
return
}
func main() {
sum, _ := add(2, 3)
}
可变参数
func calc_v1(b ...int) (sum int, sub int) {
sum:=0
for i :=0; i
b. defer语句 延迟执行语句,在函数返回之后会调用,用来释放一些资源。
func calc_v1(b ...int) (sum int, sub int) {
defer fmt.Println(“defer”)
return
}
func testDefer () {
defer fmt.PrintIn("hello")
fmt.PrintIn("aaaa")
fmt.PrintIn("bbbb")
}
打印顺序是 aaaa bbbb hello
多个defer语句,遵循栈的特性:先进后出。
func calc_v1(b ...int) (sum int, sub int) {
defer fmt.Println(“defer1”)
defer fmt.Println(“defer2”)
return
}
打印顺序是 defer2 defer1
内置函数
close:主要⽤来关闭channel
len:⽤来⻓度,⽐如string、array、slice、map、channel
new:⽤来分配内存,主要⽤来分配值类型,⽐如int、struct。返回的是指针
make:⽤来分配内存,主要⽤来分配引用类型,⽐如chan、map、slice
append:⽤来追加元素到数组、slice中
panic和recover:⽤来做错误处理
函数变量作用域和可见性
全局变量,在程序整个⽣命周期有效。
var a int = 100
局部变量,分为两种:1)函数内定义,2)语句块内定义。函数执行后就销毁。
func add(a int, b int) int {
var sum int = 0
//sum是局部变量量
if a > 0 {
var c int = 100
//c是局部变量量,仅在if语句句块有效
}
}
for d:=100;d>0 {
}
for 循环结束 d也被销毁
可⻅性,包内任何变量或函数都是能访问的。包外的话,⾸字⺟⼤写是可导出的 能够被其他包访问或调用。⼩写表示是私有的,不能被外部的包访问。
func add(a int, b int) int {
}
//add这个函数只能在包内部调用,是私有的,不能被外部的包调⽤
var a int = 100 仅在当前包调用
var A int = 200 可以在其他包使用
匿名函数,即没有名字的函数
f1:= func (a,b int) int {
return a + b
}
func test () {
var i int =0
defer fmt.PrintIn("i=%d\n",i)
i =100
fmt.PrintIn("i=%d\n",i)
return
}
打印结果 i=100 i=0,当defer执行的时候 i就被赋值了,所以ki为0
改成
func test () {
var i int =0
defer func () {
fmt.PrintIn("i=%d\n",i)
}()
i =100
fmt.PrintIn("i=%d\n",i)
return
}
当前匿名函数执行的时候i就变为100了,所以两次输出的都是100
func AnonyTest3(){
var i=0
defer func() {
fmt.Printf("defer func i=%v \n",i)
}()
defer fmt.Printf("defer i=%v \n",i)
for;i<10; i++{
}
fmt.Printf("i=%v \n",i)
}
defer fmt.Printf("defer i=%v \n",i) 打印的就是i初始化后的值,最后一个也一定是for循环之后的值10。
主要就是匿名函数执行之后的值,有意思是10,说明访问了匿名函数外部的i,这就涉及到了闭包。
i=10
defer i=0
defer func i=10
函数作为参数 Go语言中函数也是一种类型,所以可以用一个函数类型的变量进行接收。
func Calc(a,b int, op func(int,int)int) int {
return op(a,b)
}
func add(a,b int) int{
return a+b
}
func sub(a,b int)int{
return a-b
}
func AnonyTest4(){
var a = 2
var b = 1
var x = Calc(a,b,add)
var y = Calc(a,b,sub)
fmt.Printf("x=%v, y=%v \n",x,y)
}
x=3, y=1
⼀个函数和与其相关的引⽤环境组合⽽成的实体
package main
import “fmt”
func main() {
var f = Adder()
fmt.Print(f(1),” - “)
fmt.Print(f(20),” - “)
fmt.Print(f(300))
}
func Adder() func(int) int {
var x int
return func(d int) int {
x += d
return x
}
}
闭包实例:
a. 实例1
func Adder2(base int) func(int)int{
return func(i int) int{
base += i
return base
}
}
func main(){
tmp1 := Adder2(10)
fmt.Println(tmp1(1),tmp1(2))
tmp2 := Adder2(100)
fmt.Println(tmp2(10),tmp2(20))
这里Adder2接收一个int类型参数base,然后返回一个func,这里这个匿名函数里面引用了这个参数base,那么这个参数base和匿名函数就形成了一个整体。
后面我们 tmp1被赋值为 Adder2(10) ,那么在tmp1这个对象的生命周期内,base是被初始化为10且一直存在,所以结果是 11 和 13,同理后面是 110 和 130。
b. 实例2
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2))
fmt.Println(f1(3), f2(4))
fmt.Println(f1(5), f2(6))
fmt.Println(f1(7), f2(8))
}
分析一下:
这里base和 add以及sub的匿名函数也组成了一个实体也就是calc,所以在f1和f2的生命周期内,base一直存在,并被初始化成了10。
所以结果就是 f1(1) 就是10+1 =11 而 f2(2)就是 11-2 = 9,其他同理。
所以结果如下:
11 9
12 8
13 7
14 6
c. 闭包的副作用:
package main
import ( "fmt" "time")
func main() {
for i:=0; i<5; i++ {
go func(){
fmt.Println(i)
}()
}
time.Sleep(time.Second)
}
只会打印 5, 如果想要 0-4 应该将i传入匿名函数
上述代码应该结果是多少?我的猜想应该是0、1、2、3、4
但是实际结果是:5 5 5 5 5
为什么会出现这样的情况?实际上面这里每一个go协程中的匿名函数和外部for循环的i也形成了闭包,因为for循环执行比较快,所以go还没来得及执行就变成5了。我在每一个go协程之后加一个延时,结果就是0,1,2,3,4了。
func main(){
for i:=0;i<5;i++{
go func(){
fmt.Println(i)
}()
time.Sleep(time.Second)
}
time.Sleep(time.Second)
}
0 1 2 3 4
问题就在于不可能每次执行都进行延迟吧,所以需要做一件事情打破这个闭包。
func main(){
for i:=0;i<5;i++{
go func(x int){
fmt.Println(x)
}(i)
}
time.Sleep(time.Second)
}
这里把i当做参数传入到匿名函数中,保证了每次循环传的值都不一样。
推荐阅读
(点击标题可跳转阅读)
RxJS入门
一文掌握Webpack编译流程
一文深度剖析Axios源码
Javascript条件逻辑设计重构
Promise知识点自测
你不知道的React Diff
你不知道的GIT神操作
程序中代码坏味道(上)
程序中代码坏味道(下)
学习Less,看这篇就够了
一文掌握Linux实战技能-系统管理篇
一文掌握Linux实战技能-系统操作篇
一文达到Mysql实战水平
一文达到Mysql实战水平-习题答案
从表单抽象到表单中台
实战LeetCode 系列(一) (题目+解析)
一文掌握Javascript函数式编程重点
实战LeetCode - 前端面试必备二叉树算法
阿里、网易、滴滴、今日头条、有赞.....等20家面试真题
30分钟学会 snabbdom 源码,实现精简的 Virtual DOM 库
觉得本文对你有帮助?请分享给更多人
关注「React中文社区」加星标,每天进步