编辑
2021-02-01
编程
00

目录

1.入门
1. 1 Hello, World
1.2 命令行参数
2. 程序结构
2.1 命名
2.1.1 规则
2.1.2 关键字(25个)
2.1.3 预定义名字
2.1.4 作用域
2.1.5 命名习惯
2.2 声明
2.3 变量
2.3.1 语法
2.3.2 简短变量声明
2.3.3 指针
2.3.4 new函数
2.3.5 变量的生命周期
2.4 赋值
2.4.1 普通赋值
2.4.2 元组赋值
2.4.3 可赋值性
2.5 类型
2.6 包和文件
2.6.1 导入包
2.6.2 包的初始化
2.7 作用域
3. 基础数据类型
3.1 整型
3.1.1 数据类型
3.1.2 运算符
3.1.3 类型转换
3.1.4 进制
3.1.5 输出
3.2 浮点型
3.2.1 数据类型
3.2.2 输出
3.2.3 math 包
3.3 复数
3.4 布尔类型
3.5 字符串
3.5.1 字符串面值
3.5.2 Unicode
3.5.3 UTF-8
3.5.4. 字符串和Byte切片
3.5.5. 字符串和数字的转换
3.6 常量
3.6.1 基本语法
3.6.2 iota 常量生成器
4. 复合数据类型
4.1 数组
4.2 Slice
4.2.1 基础语法
4.2.2 append函数
4.2.3 Slice内存技巧
4.3 Map
4.4 结构体
4.4.1 基础语法
4.4.2 结构体字面值
4.4.3 结构体比较
4.4.4 结构体嵌入和匿名成员
4.5 JSON
5.函数
5.1 函数的声明
5.2 递归
5.3 多返回值
5.4.1 错误处理策略
5.5. 函数值
5.6 匿名函数
5.7 可变参数
5.8 Deferred 函数
5.9 Panic 异常
5.10 Recover 捕获异常
6. 方法
6.1 方法声明
6.2 基于指针对象的方法
n. 总结
函数

课本: 《Go语言圣经》

1.入门

1. 1 Hello, World

helloworld.go

go
package main import "fmt" func main() { fmt.Println("Hello, 世界") }

Go是一门编译型语言

  • Go语言的工具链将源代码及其依赖转换成计算机的机器指令(静态编译)

  • Go语言原生支持Unicode,它可以处理全世界任何语言的文本

  • go 简单命令使用

    • run 子命令: 编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件。

      • 直接输出结果

      $ go run helloworld.go

      Hello, 世界

    • build 子命令:保存编译结果

      • 生成可执行的二进制文件(在 windowns 下为exe文件)

      $ go build helloworld.go


      生成 helloworld.exe文件


      $ helloworld.exe

      Hello, 世界

    • get 子命令:通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装

      go get gopl.io/ch1/helloworld

      • 格式为:go get {域名}/{作者或机构(远程包路径格式)}/{项目名}
      • 本地需要安装对应的管理工具,如git, svn 等
      • 托管项目的网站需要支持
      • 拉取到环境变量 GOPATH 下,src 目录保存源码,bin 目录为编译后二进制文件
  • 程序结构

    • 代码通过package)组织:类型其他语言的库(libraries)或者模块(modules
      • 包由位于单个目录下的一个或多个.go源代码文件组成
      • 目录定义包的作用
      • 每个源文件都以一条package声明语句开始,这个例子里就是package main
    • 使用 import 导入包
      • 导入不需要的包,或者缺少必要的包,都会编译不通过
    • main包比较特殊。它定义了一个独立可执行的程序,而不是一个库
      • main包里的main 函数 也很特殊,它是整个程序执行时的入口
    • 函数由func关键字、函数名、参数列表、返回值列表(这个例子里的main函数参数列表和返回值都是空的)以及包含在大括号里的函数体组成
  • 工具

    • gofmt 格式化代码

      • go 不需要添加;除非一行里面又多句代码
    • goimports,可以根据代码需要,自动地添加或删除import声明

      • 需要安装

      $ go get golang.org/x/tools/cmd/goimports

1.2 命令行参数

os包以跨平台的方式,提供了一些与操作系统交互的函数和变量

  • 程序的命令行参数可从os包的Args变量获取;
  • os包外部使用os.Args访问该变量

os.Args 变量是一个字符串(string)的切片(slice)

  • os.Args[0],是命令本身的名字

  • 其它的元素则是程序启动时传给它的参数

空标识符(blank identifier),即_(也就是下划线)。空标识符可用于在任何语法需要变量名但程序逻辑不需要的时候(如:在循环里)丢弃不需要的循环索引,并保留元素值。

2. 程序结构

2.1 命名

2.1.1 规则

  • 必须以一个字母(Unicode字母)或下划线开头
  • 后面可以跟任意数量的字母、数字或下划线
  • 大写字母和小写字母是不同的

2.1.2 关键字(25个)

不能用于自定义名字,只能在特定语法结构中使用

break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var

2.1.3 预定义名字

可以使用它们。在一些特殊的场景中重新定义它们也是有意义的。

内建常量: true false iota nil 内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error 内建函数: make len cap new append copy close delete complex real imag panic recover

比如:

named.go

go
package main import ( "fmt" ) func main() { var true string true = "10" fmt.Println(true) }

$ gofmt -w named.go

$ go run named.go 10

2.1.4 作用域

  • 名字是在函数内部定义,那么它就只在函数内部有效
  • 函数外部定义,那么将在当前包的所有文件中都可以访问
  • 名字的开头字母的大小写决定了名字在包外的可见性
  • 一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问
声明作用域
函数内部定义函数内部
函数外部定义当前包的所有文件
函数外部定义(大写字母开头)外部的包访问

2.1.5 命名习惯

  • 包本身的名字一般总是用小写字母
  • 推荐使用 驼峰式 命名
  • 尽量使用短小的名字

2.2 声明

主要的四种声明

格式说明
var变量
const常量
type类型
func函数

2.3 变量

2.3.1 语法

语法:

var 变量名字 类型 = 表达式

  • 类型”或“= 表达式”两个部分可以省略其中的一个

    • 省略类型信息:将根据初始化表达式来推导变量的类型信息

    • 省略初始化表达式:将用零值初始化该变量

      • 数据类型的零值
      类型零值
      数值类型0
      布尔类型false
      字符串类型""
      接口或引用类型
      (包括slice、指针、map、chan和函数)
      nil
      数组或结构体等聚合类型每个元素或字段都是对应该类型的零值
    • Go语言中不存在未初始化的变量

  • 可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量

    go
    var i, j, k int // int, int, int var b, f, s = true, 2.3, "four" // bool, float64, string
  • 初始化表达式可以是字面量或任意的表达式

  • 包级别声明的变量会在main入口函数执行前完成初始化局部变量将在声明语句被执行到的时候完成初始化。

总结: var 变量名 [类型] [=表达式] ,[]可以省略其中一个, 需要关注初始化0值,可以同时声明(+初始化)多个变量

2.3.2 简短变量声明

函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量

名字 := 表达式

go
anim := gif.GIF{LoopCount: nframes} freq := rand.Float64() * 3.0 t := 0.0 i, j := 0, 1

“:=”是一个变量声明语句,而“=”是一个变量赋值操作

  • 简短变量和 var 一样可以同时声明多个变量

  • 简短变量声明左边的变量可能并不是全部都是刚刚声明的

  • 简短变量声明语句中必须至少要声明一个新的变量

总结: **:=与var几近等价,:=的左边只需要一个是新的变量即可,容易语义混乱

2.3.3 指针

一个变量对应一个保存了变量对应类型值的内存空间。

  • 通过指针,我们可以直接读或更新对应变量的值,而不需要知道该变量的名字

  • 如果用“var x int”声明语句声明一个x变量,那么 &x 表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。

    • *p表达式对应p指针指向的变量的值
    go
    x := 1 p := &x // p, of type *int, points to x fmt.Println(*p) // "1" *p = 2 // equivalent to x = 2 fmt.Println(x) // "2"
  • 任何类型的指针的零值都是nil

  • 返回函数中局部变量的地址也是安全的。

  • 如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值

cs.go

go
package main import ( "fmt" ) func main() { var s int = 0 incr(&s) fmt.Println(incr(&s)) var s2 int = 0 incr2(s2) fmt.Println(incr2(s2)) } // 使用指针直接操作对应地址 func incr(p *int) int { *p++ return *p } func incr2(p int) int { p++ return p; }

$ go run cs.go 2 1

  • 对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名

总结:**&变量名 获取地址,指针名 获取指向的值, 类型 为对应的指针类型

2.3.4 new函数

另一个创建变量的方法是调用内建的new函数

  • 表达式 new(T) 将创建一个T类型的匿名变量
  • 初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
go
p := new(int) // p, *int 类型, 指向匿名的 int 变量 fmt.Println(*p) // "0" *p = 2 // 设置 int 匿名变量的值为 2 fmt.Println(*p) // "2"
  • new函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活
  • new只是一个预定义的函数,它并不是一个关键字,因此我们可以将new名字重新定义为别的类型

总结:用的少,初始化为0值,返回变量地址(指针)

2.3.5 变量的生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间段

变量生命周期说明
包一级声明的变量和整个程序的运行周期是一致的
局部变量的生命周期动态的
声明语句开始
不再被引用为止
存储空间可能被回收
函数的参数变量
和返回值变量都是局部变量
它们在函数每次被调用的时候创建

将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收

总结: 没啥好说的。

2.4 赋值

2.4.1 普通赋值

使用赋值语句可以更新一个变量的值

  • 普通形式

变量 = 新值表达式

  • 特定的二元算术运算符和赋值语句的复合操作

变量 += 表达式

等价于

变量 = 变量 + 表达式

  • 自增和自减

v : = 1

v++ // 等价方式 v = v + 1;v 变成 2 v-- // 等价方式 v = v - 1;v 变成 1

2.4.2 元组赋值

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值

先计算右边所有表达式的值,然后统一更新左边的值

  • 交换变量的值

    go
    i, j = j, i x[i], x[j] = x[j],x[i]
  • 求最大公约数(GCD)

    go
    func gcd(x, y int) int { for y != 0 { x, y = y, x%y } return x }
  • 斐波那契数列(Fibonacci)

    go
    func fib(n int) int { x, y := 0, 1 for i := 0; i < n; i++ { x, y = y, x+y } return x }
  • 元组赋值也可以使一系列琐碎赋值更加紧凑

    go
    i, j, k = 2, 3, 5
  • 左右数量必须对等。

总结: 先计算右边表达式,再赋值给左边,使用起来更加方便,可以交换变量

2.4.3 可赋值性

赋值语句是显式的赋值形式,除了显示赋值外,还存在隐式赋值

隐式赋值

  • 函数出入参

  • 复合类型的字面量

    go
    medals := []string{"gold", "silver", "bronze"} //等价于 medals[0] = "gold" medals[1] = "silver" medals[2] = "bronze"

可赋值性

  • 类型必须完全匹配
  • nil 可以赋值给任何指针或引用类型的变量

总结:

2.5 类型

类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集

type 类型名字 底层类型

  • 创建一个新的类型,其底层结构和底层类型一致

  • 相同底层类型的类型并不相互兼容(包括条件判断都不兼容),只和其他未显示声明的数据兼容

  • 类型声明语句一般出现在包一级

    go
    var c Celsius var f Fahrenheit fmt.Println(c == 0) // "true" fmt.Println(f >= 0) // "true" fmt.Println(c == f) // compile error: type mismatch fmt.Println(c == Celsius(f)) // "true"!

func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

  • 类型为 Celsius 的 String() 函数,传入 c 可用

    go
    fmt.Println(c.String())
  • 例如:

    • tempconv.go

      go
      package main import "fmt" type Celsius float64 // 摄氏温度 type Fahrenheit float64 // 华氏温度 type MyInt int const ( AbsoluteZeroC Celsius = -273.15 // 绝对零度 FreezingC Celsius = 0 // 结冰点温度 BoilingC Celsius = 100 // 沸水温度 ) func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) } func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) } func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) } func (t MyInt) String() string { return "MyInt:" + fmt.Sprintf("%d",t) } func main() { fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C boilingF := CToF(BoilingC) fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F // fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch var c Celsius var f Fahrenheit fmt.Println(c == 0) // "true" fmt.Println(f >= 0) // "true" // fmt.Println(c == f) // compile error: type mismatch fmt.Println(c == Celsius(f)) // "true"! c = FToC(212.0) fmt.Println(c.String()) // "100°C" fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly fmt.Printf("%s\n", c) // "100°C" fmt.Println(c) // "100°C" fmt.Printf("%g\n", c) // "100"; does not call String fmt.Println(float64(c)) // "100"; does not call String var s MyInt = 1 fmt.Println(s) fmt.Println(s.String()) }

      $ go run tempconv.go 100 180 true true true 100°C 100°C 100°C 100°C 100 100 MyInt:1 MyInt:1

总结:

2.6 包和文件

Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用

  • 一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中

  • 每个包都对应一个独立的名字空间

  • 包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息

  • 包级别的常量名都是以大写字母开头,可以被外部访问

  • 每个源文件的包声明前紧跟着的注释是包注释

    • 一个包通常只有一个源文件有包注释
    • 如果包注释很大,通常会放到一个独立的doc.go文件中

注意:

$ go build 文件目录

  • 使用 go build 编译包

  • 文件目录为 $GOPATH/src/[目录]

  • 如:

    # $GOPATH/src/test/pk 使用以下进行编译

    $ go build test/pk

  • 一般目录后缀和包名一致(约定俗成,可以不一致)

总结:

2.6.1 导入包

每个包都有一个全局唯一的导入路径

import(

​ "GOPATH下src的相对目录"

)

  • 默认情况下导入的包绑定到包声明语句指定的名字
  • 导入包时必须使用,否则编译不通过

例子

$GOPATH/src/test/pk/t1.go

go
/* 注释:xxxx */ package yuitest const ( Test1 string = "Test1" ) func t1() string { return "t1" } func T1Export() string { return "t1 export" }

$GOPATH/src/test/pk/t2.go

go
package yuitest func t2() string { return "t2" } func T2Export() string { return "t2 export" }

$GOPATH/study/ch2/testpackage.go

go
package main import ( "fmt" "test/pk" ) func main() { fmt.Println(yuitest.Test1) fmt.Println(yuitest.T1Export()) fmt.Println(yuitest.T2Export()) }

$ go run testpackage.go Test1 t1 export t2 export

  • 这个例子里面,目录名和包名并没有一致

  • 如果一个目录放不同包导入会编译异常

    go
    $ go build test/pk can't load package: package test/pk: found packages yuitests (s1.go) and yuitest (t1.go) in D:\ProjectsOfGolang\src\test\pk

总结:

2.6.2 包的初始化

包的初始化首先是解决包级变量的依赖顺序

go
var a = b + c // a 第三个初始化, 为 3 var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c) var c = 1 // c 第一个初始化, 为 1 func f() int { return c + 1 }
  • 包中含有多个.go源文件,将.go文件根据文件名排序,然后依次调用编译器编译。

  • 包级别声明的变量初始化表达式则用表达式初始化,或用init初始化函数

  • 每个文件都可以包含多个init初始化函数

    func init() { /* ... */ }

  • init初始化函数除了不能被调用或引用

  • 每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。

  • main包最后被初始化

总结: 按依赖顺序初始化,先初始化包级变量,调用 init 函数,最后才是 main 包

2.7 作用域

声明语句的作用域是指源代码中可以有效使用这个名字的范围。

  • 作用域和生命周期不能混为一谈
    • 作用域是编译时属性,生命周期是运行时概念
  • 声明语句对应的词法域决定了作用域范围的大小
    • 全局作用域:整个程序中直接使用
      • 内置的类型、函数和常量(如 int、len和true)
    • 包级语法域:同一个包的任何源文件中访问的
      • 函数外部声明的名字
    • 源文件级的作用域:当前的文件中访问导入的fmt包
      • 导入的包
    • 局部作用域: 只能在函数内部访问
      • 函数中的声明,变量
  • 不同的词法域可以存着过个同名的声明
  • 编译器遇到一个名字引用时,从最内层向全局的作用域查找定义
  • 嵌套的代码里,内部声明会屏蔽外部的声明

3. 基础数据类型

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

基础类型,包括:数字、字符串和布尔型

数值类型包括几种不同大小的整数、浮点数和复数

3.1 整型

3.1.1 数据类型

数据类型大小(bit)无符号表示说明
int88uint8--
int1616uint16--
int3232uint32--
int6464uint64--
int32或64uint不同的编译器,不同的平台
不一样
rune32--等价于 int32
通常用于表示一个Unicode码点
byte8--uint8类型的等价类型
一般用于强调数值是
一个原始的数据而不是
一个小的整数
uintptr足以容纳指针--只有在底层编程时才需要
  • int、uint和uintptr是不同类型的兄弟类型

  • 有符号整数采用2的补码形式表,最高bit位用来表示符号位

  • 有符号数取值范围为 :

    2n12n11-2^{n-1} 到 2^{n-1}-1
  • 无符号取值范围为

    02n10 到 2^n-1

3.1.2 运算符

text
* / % << >> & &^ + - | ^ == != < <= > >= && ||
  • 优先级
    • 同一个优先级,使用左优先结合规则
    • 括号可以明确优先顺序,提升优先级
  • 前两行的运算符可以与 = 好结合,简化赋值语句
  • +-*/可以适用于整数、浮点数和复数,但是取模运算符%仅用于整数间的运算
  • %取模运算符的符号和被取模数的符号总是一致的

**即使数值本身不可能出现负数,也倾向于使用有符号的int类型,**就像数组的长度那样,虽然使用uint无符号类型似乎是一个更合理的选择

3.1.3 类型转换

  • 一般来说,需要一个显式的转换将一个值从一种类型转化为另一种类型
  • 算术和逻辑运算的二元操作中必须是相同的类型
  • 将一个大尺寸的整数类型转为一个小尺寸的整数类型,或者是将一个浮点数转为整数,可能会改变数值或丢失精度

3.1.4 进制

  • 八进制: 以 0 开头书写,如:0666;通常用于POSIX操作系统上的文件访问权限标志
  • 十六进制:以 0x0X 开头书写,如 0xdef;强调数字值的bit位模式

3.1.5 输出

  • 用%d、%o或%x参数控制输出的进制格式
go
o := 0666 fmt.Printf("%d %[1]o %#[1]o\n", o) // "438 666 0666" x := int64(0xdeadbeef) fmt.Printf("%d %[1]x %#[1]x %#[1]X\n", x) // Output: // 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
  • fmt 在默认情况下 % 对应输出对应位参数

  • %[n] n代表指定操作数,从1开始

  • %# #表示带着前缀,0、0x、0X 输出

3.2 浮点型

3.2.1 数据类型

数据类型大小(bit)极限值说明
float3232math.MaxFloat32约6个十进制数的精度
float6464math.MaxFloat64约15个十进制数的精度

通常应该优先使用float64类型

  • 科学计数法表示:通过e或E来指定指数部分
    • 如:6.02214129e23,6.62606957e-34

3.2.2 输出

  • 用Printf函数的 %g 参数打印浮点数,更紧凑的表示形式
  • 对应表格的数据,使用%e(带指数)或%f的形式打印可能更合适
go
for x := 0; x < 8; x++ { fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x))) }

output:

x = 0 e^x = 1.000 x = 1 e^x = 2.718 x = 2 e^x = 7.389 x = 3 e^x = 20.086 x = 4 e^x = 54.598 x = 5 e^x = 148.413 x = 6 e^x = 403.429 x = 7 e^x = 1096.633

3.2.3 math 包

  • +Inf -Inf: 正无穷大和负无穷大,分别用于表示太大溢出的数字和除零的结果
  • NaN非数,一般用于表示无效的除法操作结果0/0或Sqrt(-1).

3.3 复数

数据类型

数据类型大小说明
complex6464对应float32
complex128128对应float64
  • 内置的complex函数用于构建复数

  • 内建的real和imag函数分别返回复数的实部和虚部

    go
    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"
  • 一个浮点数面值或一个十进制整数面值后面跟着一个i,将构成一个复数的虚部,复数的实部是0:

  • 简化声明

    go
    x := 1 + 2i // 1+2i y := 3 + 4i // 3+4i
  • 复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的

3.4 布尔类型

一个布尔类型的值只有两种:true和false。

  • ! : !true = false
  • =<等比较操作会产生布尔型的值
  • 布尔值可以和&&(AND)和||(OR)操作符结合,并且有短路行为
  • &&的优先级比|| (&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高)

3.5 字符串

一个字符串是一个不可改变的字节序列

  • 内置的len函数可以返回一个字符串中的字节数目
  • 引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i < len(s)条件约束。(第i个字节并不一定是字符串的第i个字符)
  • 试图访问超出字符串索引范围的字节将会导致panic异常
  • 子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串,新字符串将包含j-i个字节
  • +操作符将两个字符串连接构造一个新字符串
  • 字符串可以用==<进行比较
    • 逐个字节比较
    • 结果是字符串自然编码的顺序
  • 字符串的值是不可变的
    • s[i] = 'l' // compile error: cannot assign to s[i]

3.5.1 字符串面值

字符串值也可以用字符串面值方式编写

"Hello world"

转义字符

text
\a 响铃 \b 退格 \f 换页 \n 换行 \r 回车 \t 制表符 \v 垂直制表符 \' 单引号(只用在 '\'' 形式的rune符号面值中) \" 双引号(只用在 "..." 形式的字符串面值中) \\ 反斜杠

进制

text
十六进制以 \xhh 形式标识,h 代表十六进制数,大小写都可,如 \xDf 八进制 \ooo 形式表示,只有三位,不可以超过 \377

原生字符串

反引号代替双引号

go
` 在原生的字符串面值中,没有转义操作; 全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行 原生字符串面值中无法使用符号 可以用八进制或十六进制转义或+"`"连接字符串常量完成 `

3.5.2 Unicode

unicode: http://unicode.org

  • 每个符号都分配一个唯一的Unicode码点
  • Unicode码点对应Go语言中的rune整数类型
  • 通用的表示一个Unicode码点的数据类型是int32,也就是Go语言中rune对应的类型

3.5.3 UTF-8

  • UTF8是一个将Unicode码点编码为字节序列的变长编码
  • UTF8编码使用1到4个字节来表示每个Unicode码点
    • ASCII部分字符只使用1个字节
    • 常用字符部分使用2或3个字节表示
  • 数据结构
    • 第一个字节的高端bit位用于表示编码总共有多少个字节
      • 0,则表示对应7bit的ASCII字符
      • 高端bit是110,则说明需要2个字节
      • 需要n字节,n位1开头,n+1位为0
    • 后续的每个高端bit都以10开头
text
0xxxxxxx runes 0-127 (ASCII) 110xxxxx 10xxxxxx 128-2047 (values <128 unused) 1110xxxx 10xxxxxx 10xxxxxx 2048-65535 (values <2048 unused) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536-0x10ffff (other values unused)
  • 字符串面值中的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符。有两种形式:\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字;一般很少需要使用32bit的形式。每一个对应码点的UTF8编码。
  • 对于小于256的码点值可以写在一个十六进制转义字节中,例如\x41对应字符'A',但是对于更大的码点则必须使用\u\U转义形式。

总结:

Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储

UTF-8 是 Unicode 的实现方式之一

3.5.4. 字符串和Byte切片

标准库中有四个包对字符串处理尤为重要:bytesstringsstrconvunicode包。

  • strings 包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
  • bytes 包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型
  • strconv 包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换
  • unicode 包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类

字符串和字节slice之间可以相互转换

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

3.5.5. 字符串和数字的转换

整数转为字符串

  • 用fmt.Sprintf返回一个格式化的字符串
  • 用strconv.Itoa(“整数到ASCII”)
    • FormatInt和FormatUint函数可以用不同的进制来格式化数字
go
x := 123 y := fmt.Sprintf("%d", x) fmt.Println(y, strconv.Itoa(x)) // "123 123" fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
  • fmt.Printf函数的%b%d%o%x等参数提供功能往往比strconv包的Format函数方便很多

3.6 常量

3.6.1 基本语法

常量表达式的值在编译期计算,而不是在运行期。

  • 每种常量的潜在类型都是基础类型:booleanstring数字

  • 常量的值不可修改

  • 语法

    const <name> [type] = <value>

    const ( <name1> [type] = <value1>

    ​ <name2> [type]= <value2>

    )

    // 类型可以省略

  • 常量可以是构成类型的一部分,如指定数组长度

  • 没有显式指明类型,那么将从右边的表达式推断类型

  • 批量声明时,第一个常量不可以省略初始化,其他的如果省略初始化则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的

3.6.2 iota 常量生成器

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。

  • 在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一

    go
    package main import "fmt" type Weekday int const ( t1 = 1 t2 s1 Weekday = iota s2 s3 s4 ) const ( ss1 Weekday = iota ss2 ss3 ss4 ) func main() { fmt.Println(s1, s2, s3, s4) fmt.Println("----------------------") fmt.Println(ss1, ss2, ss3, ss4) }

    $ go run testConst.go

    2 3 4 5

    ----------------------

    0 1 2 3

  • iota 可以理解为第n-1常量,可以再表达式中使用

    go
    const ( t1 = 2 * iota //0 t2 //2 t3 //4 )

3.6.3 无类型常量

编译器为常量提供比基础类型更高精度的算术运算(256bit+的运算精度)

  • 六种未明确确定类型的常量:
    • 无类型的布尔型
    • 无类型的整数
    • 无类型的字符
    • 无类型的浮点数
    • 无类型的复数
    • 无类型的字符串
  • 可以直接用于更多的表达式而不需要显式的类型转换
  • 常量面值,不同的写法可能会对应不同的类型
  • 只有常量可以是无类型的。赋值给变量时会隐式地进行类型转换

4. 复合数据类型

主要学习四种复合类型:数组、slice、map和结构体

4.1 数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成

  • 数组的长度是固定的

  • 数组的每个元素可以通过索引下标来访问

    • n个元素的数组,索引下标的范围是从 0 ~ n-1
  • 内置的len函数将返回数组中元素的个数

  • 顺序初始化值

    go
    // 声明初始化 var q [3]int = [3]int{1, 2, 3} // 字面值初始化中,由字面值数量决定数组长度 q1 := [...]int{1,2,3}
  • 数组的长度必须是常量表达式

  • 如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的

    • 长度一致
    • 元素类型一致
    • 元素类型可比较
  • Printf 函数的%x副词参数,它用于指定以十六进制的格式打印数组或slice全部的元素

    • %T副词参数是用于显示一个值对应的数据类型
    • %t副词参数是用于打印布尔型数据
  • 函数入参是原数据的副本,所以传入较大的数组是效率会比较低下

    • 可以通过指针进行传递

      go
      package main import "fmt" func main() { var t1 [3]int = [3]int{1, 2, 3} fmt.Println(t1) change1(t1) fmt.Println(t1) change2(&t1) fmt.Println(t1) } func change1(data [3]int) { data[1] = 100 fmt.Println(data) } func change2(data *[3]int) { (*data)[1] = 100 fmt.Println(*data) }

      $ go run testArray.go [1 2 3] [1 100 3] [1 2 3] [1 100 3] [1 100 3]

4.2 Slice

4.2.1 基础语法

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。

  • 一个slice类型一般写作[]T,其中T代表slice中元素的类型
  • slice的语法和数组很像,只是没有固定长度而已
  • 底层用了一个数组对象
  • 个slice由三个部分构成:指针、长度和容量
    • 指针指向第一个slice元素对应的底层数组元素的地址,slice的第一个元素并不一定就是数组的第一个元素
    • 长度对应slice中元素的数目
    • 长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置
    • 内置的len和cap函数分别返回slice的长度和容量
  • 扩容: 对 slice 再一次 slice 可以超过原来 slice 的长度
go
package main import "fmt" func main() { q := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9} s := q[1:4] fmt.Println(s) fmt.Println(len(s)) fmt.Println(cap(s)) s1 := s[:4] fmt.Println(s1) }

$ go run testSlice.go [2 3 4] 3 8 [2 3 4 5]

  • slice 不能使用 == 进行判断,byte类型可以使用 bytes.Equal 函数来判断,其他类型需要展开比较
  • 为什么 slice 不能使用 == 比较
    • slice 的元素是间接引用的,slice甚至可以包含自身(声明为[]interface{}时,slice的元素可以是自身)
    • 一个固定的slice值,不同的时刻可能包含不同的元素(底层数组被修改时)
  • slice 唯一合法的比较操作是和nil比较
  • 一个零值的slice等于nil

4.2.2 append函数

通常是将append返回的结果直接赋值给输入的slice变量

实际上对应任何可能导致长度、容量或底层数组变化的操作重新把输出结果赋值给变量都是必要的。

4.2.3 Slice内存技巧

注意:尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的

4.3 Map

一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value

  • K对应的key必须是支持==比较运算符的数据类型

  • 内置的make函数可以创建一个map

    go
    ages := make(map[string]int) // mapping from strings to ints
  • map字面值的语法创建map

    go
    ages := map[string]int{ "alice": 31, "charlie": 34, } // 等价于 ages := make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34
  • 创建空的map的表达式:

    go
    map[string]int{}
  • Map中的元素通过key对应的下标语法访问

  • 使用内置的delete函数可以删除元素:

    go
    delete(ages, "alice") // remove element ages["alice"]
  • 查找失败将返回value类型对应的零值

    • x += yx++等简短赋值语法也可以用在map

      go
      ages["bob"] += 1 ages["bob"] ++
    • 对于 number 类型来讲,需要区分是存在的 0 和非存在的 0(nil),可以如下处理

      go
      age, ok := ages["bob"] if !ok {/* "bob" is not a key in this map; */}
  • map中的元素并不是一个变量,不能对map的元素进行取址操作

    go
    _ = &ages["bob"] // compile error: cannot take address of map element
  • 遍历map中全部的key/value

    • 可以使用 range 风格的for循环实现

      go
      for name, age := range ages { fmt.Printf("%s\t%d\n", name, age) }
    • map 强制每次遍历都是不同顺序的

    • 要排序时,需要使用 key 去生成一个 slice 去排序,在去调用

      go
      import "sort" var names := make([]string, 0, len(ages)) for name := range ages { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Printf("%s\t%d\n", name, ages[name]) }

注意:

  • map 下标语法(map[xxx])会产生两个返回值,第一个为value值(不存在key时,会返回类型零值),第二个是一个布尔值,用于报告元素是否真的存在

  • key 必须是可以比较的

4.4 结构体

4.4.1 基础语法

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。

go
type <StructName> struct { <filedName> <fildeType> <filedName> <fildeType> <filedName> <fildeType> ... } var <name> <StructName>
  • 结构体变量的成员可以通过点操作符访问: name.fileName

  • 点操作符也可以和指向结构体的指针一起工作

    go
    var t1 *StructName = &name t1.fileName += "test" // 等价 (*t1).fileName += "test"
  • 调用函数返回的是值,并不是一个可取地址的变量

  • 结构体成员的输入顺序也有重要的意义,不同的输入顺序构成不同的结构体

  • 一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构

4.4.2 结构体字面值

结构体值也可以用结构体字面值表示,结构体字面值可以指定每个成员的值。

  • 第一种写法
Go
type Point struct{ X, Y int } p := Point{1, 2}

上面是一种声明方式但是结构体成员有细微的调整就可能导致上述代码不能编译,因此,上述的语法一般只在定义结构体的包内部使用,或者是在较小的结构体中使用,这些结构体的成员排列比较规则

  • 第二种写法:以成员名字和相应的值来初始化,可以包含部分或全部的成员
    • 被忽略的成员默认使用零值
Go
anim := gif.GIF{LoopCount: nframes}

4.4.3 结构体比较

  • 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的
    • 用==或!=运算符进行比较

4.4.4 结构体嵌入和匿名成员

声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员

  • 繁琐的调用方式 a.b.c = xx
Go
type Point struct { X, Y int } type Circle struct { Center Point Radius int } type Wheel struct { Circle Circle Spokes int } var w Wheel w.Circle.Center.X = 8 w.Circle.Center.Y = 8 w.Circle.Radius = 5 w.Spokes = 20
  • 声明匿名成员
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 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.Radius = 5 w.Spokes = 20 // 字面值不能使用简短声明 w = Wheel{8, 8, 5, 20} // compile error: unknown fields w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

4.5 JSON

5.函数

5.1 函数的声明

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

Go
func name(parameter-list) (result-list) { body }
  • 形式参数列表描述了函数的参数名以及参数类型
  • 回值列表描述了函数返回值的变量名以及类型
  • 函数的类型被称为函数的签名。
    • 两个函数形式参数列表和返回值列表中的变量类型一一对应,那么这两个函数被认为有相同的类型或签名。
    • 形参和返回值的变量名不影响函数签名
    • 不影响它们是否可以以省略参数类型的形式表示

5.2 递归

Go语言使用可变栈,栈的大小按需增加(初始时很小)。这使得我们使用递归时不必考虑溢出和安全问题

5.3 多返回值

一个函数可以返回多个值

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

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

    Go
    links, _ := findLinks(url) // errors ignored
  • 一个函数内部可以将另一个有多返回值的函数调用作为返回值

  • 如果一个函数所有的返回值都有显式的变量名,那么该函数的return语句可以省略操作数。这称之为bare return。

    go
    func test() (i int, err string, isSuccess bool){ i = 10 err = "err" isSuccess = true // 等价于 return i, err, isSuccess return }

5.4 错误

5.4.1 错误处理策略

5.5. 函数值

函数被看作第一类值(first-class values)

  • 函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回
go
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。调用值为nil的函数值会引起panic错误:
Go
var f func(int) int f(3) // 此处f的值为nil, 会引起panic错误
  • 函数值可以与nil比较,但是函数值之间是不可比较的,也不能用函数值作为map的key。
Go
var f func(int) int if f != nil { f(3) }

5.6 匿名函数

testFunc.go

go
package main import "fmt" func main() { f := func(x int) string { fmt.Println(x, "次"); return "success" } f(1) f(2) fmt.Println(testFunc(f)) } func testFunc(print func(x int) string) string { return print(123) }

$ go run testFunc.go

1 次 2 次 123 次 success

5.7 可变参数

在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数

  • 入参在函数体中被构建成对应类型的 slice
  • 入参在调用处被隐式构建成对应类型的 数组
  • 即: 在调用可变参数的函数是,隐式构建一个 数组 ,在把 数组slice 传递给函数使用
  • 入参可以为对应类型的数据列表, 为 slice 时 需要在后面加上省略符号 "..."

testFunc.go

go
package main import "fmt" func main() { testParams(1,2,3,4) data := [4]int{2,2,3,4} testParams(data[:]...) dataSlice := []int{3,2,3,4} testParams(dataSlice...) } func testParams(t1 ...int) { fmt.Println(t1) }

$ go run testFunc.go

[1 2 3 4] [2 2 3 4] [3 2 3 4]

5.8 Deferred 函数

**延迟函数,延迟调用 defer 声明的语句 **

  • 在函数结束时执行,不管是 reutrn 结束的,还是 panic 结束

  • 一般用以关闭 io 等系统资源

  • 也常被用于记录何时进入和退出函数

testDefer.go

go
func testDefer(num ...int){ result := 0 defer fmt.Println(result) for _, value := range num { result += value } fmt.Println(result) } testDefer(1,2,3,4)

$ go run testDefer.go

10 0

进入和退出的记录

go
func bigSlowOperation() { // trace方法后面的()是必须的,如果没有,结束的时候才执行 start 的打印,而 exit 的打印不会被执行 defer trace("bigSlowOperation")() // don't forget the extra parentheses // ...lots of work… time.Sleep(10 * time.Second) // simulate slow operation by sleeping } func trace(msg string) func() { start := time.Now() log.Printf("enter %s", msg) return func() { log.Printf("exit %s (%s)", msg,time.Since(start)) } }

5.9 Panic 异常

由于panic会引起程序的崩溃,因此 panic 一般用于严重错误,如程序内部的逻辑不一致

  • 终止函数运行,执行 defer 内容,结束函数调用
  • 一般用于严重错误

panic("异常信息")

5.10 Recover 捕获异常

go
func Parse(input string) (s *Syntax, err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("internal error: %v", p) } }() // ...parser... }

6. 方法

6.1 方法声明

函数声明时,在其名字前放上一个变量,即是一个方法

testMethod.go

go
package main import "fmt" type Cat struct { Age int Name string } func (cat Cat) call() { fmt.Printf("喵喵喵~,我是 %s, 今年 %d 岁了\n", cat.Name, cat.Age) } type Cats []Cat func (cats Cats) call() { for _, cat := range cats { cat.call() } } func main() { cat := Cat{Age: 10, Name: "小花"} cat.call() cats := Cats{ {9, "红猫"}, {11, "蓝猫"}, } cats.call() }

$ go run testMethod.go

喵喵喵~,我是 小花, 今年 10 岁了

喵喵喵~,我是 红猫, 今年 9 岁了 喵喵喵~,我是 蓝猫, 今年 11 岁了

6.2 基于指针对象的方法

当调用一个函数时,会对其每一个参数值进行拷贝,如果一个函数需要更新一个变量,或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝,这种情况下我们就需要用到指针了

  • nil 指针作为其接收器 ,即 为 nil 的对象准许调用其方法
go
package main import "fmt" type Cat struct { Age int Name string } func (cat *Cat) call() { // golang 隐式的转换为 (*cat).Name, (*cat).Age fmt.Printf("喵喵喵~,我是 %s, 今年 %d 岁了\n", cat.Name, cat.Age) } func main() { cat := Cat{Age: 10, Name: "小花"} cat.call() // golang 隐式的转换为 (&cat).call(),也可以显示的去写 }

n. 总结

函数

go
// 函数声明 func name(paramName Type) returnType {} // 匿名函数 f := func (paramName Type) returnType {} f(data) // 函数作为入参 func test(fnc func(paramName Type) returnType, data Type) { fnc(data) } test(f) // 延迟函数 // 类似函数级的 finally 不同的是调用函数返回的函数,能达到先调用函数,函数结束时调用函数返回的函数,一次达到进出记录和调用的效果 defer funcCall()() // funcCall() 返回一个函数,funcCall()执行到这条语句时先被调用,所处函数结束后调用返回函数 // 异常 panic("异常") // 异常捕获,声明在 defer 中 defer func() { if p := recover(); p != nil { err = fmt.Errorf("internal error: %v", p) } }()

本文作者:Yui_HTT

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!