go语言实战_一文掌握GO语言实战技能(一) 万字长文

  • GO 基本介绍

  • 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% 的比例排在首位。

67a0ad776610760d6c1c6c8200e3ef01.png

但 Go 要想撼动编程界的常青树 Java 二十多年的地位无疑难度颇大。据 HackerRank 数据显示,2018 年,Java 在开发者最受欢迎的编程语言排行榜中仍然排名第 2,Python 排名第 4,Go 排名第 13,距离第一名 JavaScript 还有不小的差距。

GO 基本概念

  • Golang 目录结构划分

a. 个人项目

b3147ebe7a142577c09fe7092569aa13.png

b. 公司项目

74fa56c8a7d70f6f34896b3f02becab3.png
  • 包的概念

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函数是程序的执⾏⼊⼝

bdb18496f8ab8405a956ac2b096705b4.png

GO 语言特性

  • 垃圾回收

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差不多

Go 程序基本结构

a. 任何⼀个代码⽂件⾪属于⼀个包

package main 
import “fmt” 
func main() {
  fmt.Println(“hello, world”) 
}

b. import 关键字,引⽤其他包

c. 开发可执行程序,package main, 并且有且只有⼀个main⼊⼝函数

d. 包中函数调⽤

同一个包中函数,直接用函数名调, 不同包中函数,通过包名+点+ 函数名进⾏调⽤

e. 包访问控制规则

大写意味着这个函数/变量是可导出,小写意味着这个函数/变量是私有的,包外部不能访问

GO 标识符 关键字 变量 常量

  • 标识符

标识符是指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语言中还存在着一些特殊的标识符,叫做预定义标识符,如下表所示:

0d06445a8410429a54490ee230298e7d.png

预定义标识符一共有 36 个,主要包含Go语言中的基础数据类型和内置函数,这些预定义标识符也不可以当做标识符来使用。

了解规则后,判断下面标识符哪个是错误的:

A. 88ab(错)    B. _ab28     C. ab_28

  • 关键字

关键字是Go语⾔预先定义好的,有特殊含义的标识符。共25 个。

dab886379544f8b30453b491bd10ad5f.png
  • 变量

语法: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 < b
 c
)

GO 语言数据类型 与 操作符

  • 数据类型

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个类型:

0a162e6c0153ab02e116b4cd473952b8.png

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 mainimport ("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位。

通过以上,可以总结出以下几点:

  1. float32 和 float64 可以表示的数值很多

浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;

常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;

float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324。

  1. 数值很大但精度有限

人家虽然能表示的数值很大,但精度位却没有那么大。

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组成

GO 操作符 与 日期

操作符

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"))

GO 流程控制

流程控制

if condition {
   //do something
} else if condition {
     //do something
} else {
   //do something
}

或者:
if statement; condition {
}

例子:

package mainimport ( "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 mainimport ( "fmt")
func main() {
  for i:=1; i<= 10;i ++ { 
    fmt.Printf(" %d",i)
  }
}

b. break,终⽌循环

package mainimport ( "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 mainimport ( "fmt" )
func main() {
  for i :=1;i<=10;i++ {
    if i%2 == 0 {
      continue
    }
    fmt.Printf("%d", i)
  }
}  
13579
package mainimport (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 mainimport ("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")
  }
}

Go 函数介绍

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     sum = sum =b[i]
   }
   return sum
}
func calc_v2(a int, b ...int) (sum int, sub int) {
   return
}
func calc_v3(a int, b int, c ...int) (sum int, sub int) {
   return
}

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 可以在其他包使用
05b97d503102b5f83d2f840c6d43db2f.png
fd5797ef6e69dc491ff80db28a1417f8.png
  • 匿名函数,即没有名字的函数
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

GO 闭包

⼀个函数和与其相关的引⽤环境组合⽽成的实体

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 mainimport ( "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实战水平-习题答案

从表单抽象到表单中台

vue源码分析(1)- new Vue

实战LeetCode 系列(一) (题目+解析)

一文掌握Javascript函数式编程重点

实战LeetCode - 前端面试必备二叉树算法

一文读懂 React16.0-16.6 新特性(实践 思考)

阿里、网易、滴滴、今日头条、有赞.....等20家面试真题

30分钟学会 snabbdom 源码,实现精简的 Virtual DOM 库


觉得本文对你有帮助?请分享给更多人

关注「React中文社区」加星标,每天进步

a834da801dfebc48a4e168a2f56df872.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值