文章目录
- 1. 关键字
- 1.1. var:变量声明
- 1.2. const / iota
- 1.3. type:类型别名 / 类型定义
- 1.4. fallthrough
- 1.5. new / make
- 1.6. Go语言strconv包:字符串 / 数值类型的相互转换
- 1.7. 字符类型 byte / rune
- 1.8. Golang之类型转换
- 1.9. 类型断言 type assertion
- 1.10. 常量与const / iota常量生成器 / 枚举
- 1.11. 值类型/引用类型
- 1.12. defer 延迟
- 1.13. panic 宕机----程序终止运行
- 1.14. recover 宕机恢复----防止程序崩溃
- 1.15. switch...case...
- 1.16. bool 布尔值
- 1.17. 流程控制: break / continue / goto / fallthrough
- 2. 数据结构
- 2.1. 数组
- 2.2. 切片
- 2.3. map哈希表
- 结构体转map\string\interface{}的若干方法
- 2.4. 链表list --- container/list包
- 2.5. string字符串
- 3. 结构体 / 函数 / 方法 / 接口interface
- 3.1. 结构体
- 3.2. 函数:Golang 一等公民
- 3.3. 闭包(匿名函数):一个函数与其相关的引用函数组合而成的实体
- 3.4. 方法 / 接收器
- 3.5. 接口interface
- 3.6. 空接口interface{}
- 4. 编码技巧
- 4.1. 你需要知道的那些go语言json技巧
- 4.2. 结构体转map\string\interface{}的若干方法
- 4.3. Go语言中的单例模式
- 4.4. 切片splice使用技巧
写在最前,推荐几个很好的Golang学习链接
1. 关键字
1.1. var:变量声明
- 局部变量声明必须使用,不使用就编译失败
- 全局变量可以只声明,不使用
var 变量名 变量类型
变量名 := 变量值
var name string // 全局变量声明
var ( // 批量声明
age int
isOK bool
)
/* 函数外的每个语句都必须以关键字var/const/func开始*/
func test() {
ready := true // 简短声明: 只能在函数中使用
// 不能在全局变量中使用(编译失败)
}
1.2. const / iota
常量,代表永远只读,不能修改(bool、数值、string)
iota是go语言的常量计数器,只能在常量的表达式中使用
- iota在const关键字出现时,将被重置为0
- const中每新增一行,常量声明将使iota计数累加一次(iota可理解为const语句块中的航索引)
- 使用iota能简化定义,在定义枚举时很有用
const (
n1 = iota
n2
n3
)
- go 采用const+iota实现枚举
// C语言中的枚举变量enum
enum
{
SUNDAY = 0,
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY
}
// go中没有枚举类型enum,使用const代替
const (
SUNDAY = 0
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
)
1.3. type:类型别名 / 类型定义
- 类型别名
TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型,就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。
type TypeAlias = Type
类型别名/类型定义表面上看只有一个等号的差异,那么它们之间实际的区别有哪些呢?下面通过一段代码来理解
package main
import ("fmt")
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
// 将a声明为NewInt类型
var a NewInt
fmt.Printf("a type: %T\n", a) // 查看a的类型名 a type: main.NewInt
// 将a2声明为IntAlias类型
var a2 IntAlias
fmt.Printf("a2 type: %T\n", a2) // 查看a2的类型名 a2 type: int
}
- 类型定义
type byte uint8
type add_func func(int, int) int
// 声明自定义类型
type People struct {
name string
age int
}
1.4. fallthrough
1.加了fallthrough后,会直接运行【紧跟的后一个】case或default语句,不论条件是否满足都会执行
2.加了fallthrough语句后,【紧跟的后一个】case条件不能定义常量和变量
3.执行完fallthrough后直接跳到下一个条件语句,本条件执行语句后面的语句不执行
1.5. new / make
- make和new都是用来申请内存的
- new很少用,一般用来给基本数据类型申请内存,string、int,返回的是对应类型的指针(*string、*int)
- make是用来给slice、map、chan申请内存的,make函数返回的是对应这3个类型本身
TODO:补充下make创建slice、map、chan的使用案例
1.6. Go语言strconv包:字符串 / 数值类型的相互转换
Go语言中的 strconv 包为我们提供了字符串和基本数据类型之间的转换功能
- string 与 int 类型之间的转换
- Itoa(): int ==> string
func Itoa(i int) string
- Atoi(): string ==> int
func Atoi(s string) (i int, err error)
func main() {
str2 := "s100"
num2, err := strconv.Atoi(str2)
if err != nil {
fmt.Printf("%v 转换失败!", str2)
} else {
fmt.Printf("type:%T value:%#v\n", num2, num2)
}
}
// s100 转换失败!
- Parse 系列函数:字符串 ==> 指定类型的值
/* string ==> bool
* @param [in] 参数只能是 1、0、t、f、T、F、true、false、True、False、TRUE、FALSE,
* 其他的值返回错误
*/
func ParseBool(str string) (value bool, err error)
/* string ==> int
* @param [in] base 进制,范围是2~36。如果base==0,则会从字符串前置判断
* @param [in] bitSize 指定结果必须能无溢出赋值的证书类型,0、8、16、32、64,分别代表int、int8、int16、int32、int64
* @return 返回的 err 是 *NumErr 类型的,如果语法有误,err.Error = ErrSyntax,如果结果超出类型范围 err.Error = ErrRange。
*/
func ParseInt(s string, base int, bitSize int) (i int64, err error)
// ParseUint() 函数的功能类似于 ParseInt() 函数,但 ParseUint() 函数不接受正负号,用于无符号整型
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
/* string ==> float
* @param [in] bitSize 指定了返回值的类型,32 表示 float32,64 表示 float64;
* @return 返回值 err 是 *NumErr 类型的,如果语法有误 err.Error=ErrSyntax,如果返回值超出表示范围,返回值 f 为 ±Inf,err.Error= ErrRange。
*/
func ParseFloat(s string, bitSize int) (f float64, err error)
- Format系列函数:将给定类型数据==>string
// bool ==> string
func FormatBool(b bool) string
/* int ==> string
* @param [in] i 必须是int64类型
* @param [in] base 参数base必须在2~36之间,返回结果中会使用小写字母“a”到“z”表示大于 10 的数字。
*/
func FormatInt(i int64, base int) string
// 与 FormatInt() 函数的功能类似,但是参数 i 必须是无符号的 uint64 类型
func FormatUint(i uint64, base int) string
/* int ==> string
* @param [in] i 必须是int64类型
* @param [in] bitSize 表示参数 f 的来源类型(32 表示 float32、64 表示 float64),会据此进行舍入
* @param [in] fmt 表示格式,可以设置为
* “f”表示 -ddd.dddd
* “b”表示 -ddddp±ddd,指数为二进制
* “e”表示 -d.dddde±dd 十进制指数
* “E”表示 -d.ddddE±dd 十进制指数
* “g”表示指数很大时用“e”格式,否则“f”格式
* “G”表示指数很大时用“E”格式,否则“f”格式。
* @param [in] prec 控制精度(排除指数部分)
* 当参数 fmt 为“f”、“e”、“E”时,它表示小数点后的数字个数;当参数 fmt 为“g”、“G”时,
* 它控制总的数字个数。如果 prec 为 -1,则代表使用最少数量的、但又必需的数字来表示 f。
*/
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
{
var num float64 = 3.1415926
str := strconv.FormatFloat(num, 'E', -1, 64)
fmt.Printf("type:%T,value:%v\n ", str, str) // type:string,value:3.1415926E+00
}
- Append系列:① 指定类型 ==> string ② 追加到一个切片中
- Append 系列函数和 Format 系列函数的使用方法类似,只不过是将转换后的结果追加到一个切片中
- AppendBool()、AppendFloat()、AppendInt()、AppendUint()
package main
import (
"fmt"
"strconv"
)
func main() {
// 声明一个slice
b10 := []byte("int (base 10):")
// 将转换为10进制的string,追加到slice中
b10 = strconv.AppendInt(b10, -42, 10)
fmt.Println(string(b10))
b16 := []byte("int (base 16):")
b16 = strconv.AppendInt(b16, -42, 16)
fmt.Println(string(b16))
}
/*
运行结果
int (base 10):-42
int (base 16):-2a
*/
1.7. 字符类型 byte / rune
- Go中的字符有两种:①uint8类型/byte,代表了ASCII码的一个字符 ②rune类型,代表一个UTF-8字符(中文、日文、其他复合字符),处理Unicode
- byte 和 rune 都是类型的别名:使用
type 别名 = 已经存在的变量类型
string中的每一个元素叫做“字符”,GO预研的字符有以下两种
- uint8 \ byte类型:代表了ACSII码的一个字符
var ch byte = 'A' // 字符使用单引号括起来
- rune类型:等价于uint32类型。代表一个Unicode(UTF-8字符),当需要处理中文、日文或者其他复合字符时,需要用到rune类型
1.8. Golang之类型转换
- Go不存在隐式的类型转换,所有的类型转换都要显示书写,格式为:类型 B 的值 = 类型 B(类型 A 的值)
1.9. 类型断言 type assertion
- 定义
- 使用在接口值上
- 用于检查接口类型变量所持有的值,是否先实现了期望的接口或具体的类型
- 格式:
/*
* @param x 一个接口的类型
* @param T 一个具体的类型(也可为接口类型)
* @return 返回 x 的值(也就是 value)和一个布尔值(也就是 ok)
* 可以根据布尔值判断 x 是否为 T 类型
*/
value, ok := x.(T)
- 代码示例
① 简单案例
注意:
- 如果不接收第二个参数也就是下面代码中的 ok,断言失败时会直接造成一个 panic
- 如果 x 为 nil 同样也会 panic
package main
import ("fmt")
func main() {
var x interface{} // 定义接口类型
x = 10
value, ok := x.(int)
fmt.Print(value, ",", ok) // 10, ture
}
② 类型断言还可以配合 switch 使用
package main
import ("fmt")
func main() {
var a int
a = 10
getType(a)
}
func getType(a interface{}) {
switch a.(type) {
case int:
fmt.Println("the type of a is int")
case string:
fmt.Println("the type of a is string")
case float64:
fmt.Println("the type of a is float")
default:
fmt.Println("unknown type")
}
}
// the type of a is int
1.10. 常量与const / iota常量生成器 / 枚举
- 常量在编译时被创建,即使在函数内部也是如此
- 常量类型:只能是(bool、数字型、string)
- iota常量生成器
- 常量声明可以使用iota常量生成器初始化,即:它用于生成一组以相似规则初始化的常量(不用每行都写一遍初始化表达式,简化代码)
- 在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一
type Weapon int // 将 int 定义为 Weapon 类型
const (
Arrow Weapon = iota // 开始生成枚举值, 默认为0
Shuriken
SniperRifle
Rifle
Blower
)
// 输出所有枚举值
fmt.Println(Arrow, Shuriken, SniperRifle, Rifle, Blower) // 0 1 2 3 4
// 使用枚举类型并赋初值
var weapon Weapon = Blower
fmt.Println(weapon) // 4
- 将枚举值转化为字符串
package main
import "fmt"
// 声明芯片类型
type ChipType int
const (
None ChipType = iota
CPU // 中央处理器
GPU // 图形处理器
)
func (c ChipType) String() string {
switch c {
case None:
return "None"
case CPU:
return "CPU"
case GPU:
return "GPU"
}
return "N/A"
}
func main() {
// 输出CPU的值并以整型格式显示
fmt.Printf("%s %d", CPU, CPU)
}
1.11. 值类型/引用类型
值类型:变量直接存储,内存在栈上分配
基本数据类型:int/float/bool/string、数组、struct
- 值传递:函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据
引用类型:变量存储的是一个地址(指向内存),内存通常在堆上分配
指针、chan、slice/map/interface等,以引用方式传递
- 引用传递:函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方
1.12. defer 延迟
在Go语言中,return语句在底层并不是原子操作,而是分为两步:返回值赋值、RET指令
- 而,defer语句执行的时机,就是在返回值赋值操作后,RET指令执行之前
1-当函数返回时,执行defer注册的函数 ==> 可以做资源清理
func read() {
file := open(filename)
defer file.Close()
//文件操作
}
2-多个defer语句,按照先进后出的方式执行
3-defer语句中的变量,在defer声明时就决定了
// 案例1: 执行结果 0
func a() {
i := 0
defer fmt.Println(i)
i++
}
// 案例2: 执行结果 5 4 3 2 1
func f() {
for i := 1; i <= 5; i++ {
defer fmt.Printf(“%d “, i)
}
}
详细介绍defer
- 作用域:函数返回之前调用(而不是在
退出代码块作用域
之前执行) - 参数预算
defer关键字使用【传值】的方式传递参数时会进行预计算,导致不符合预期的结果
案例1
func main() {
startedAt := time.Now()
defer fmt.Println(time.Since(startedAt))
time.Sleep(time.Second)
}
/*
$ go run main.go // 错误,不符合预期
0s
*/
案例1
:defer关键字会立即拷贝函数中引用的外部参数,所以time.Since(startedAt)的结果不是main函数退出之前,而是在defer关键字调用时计算 ==> 最终导致代码输出0s
- 想要解决上面的问题非常简单==>只需要
向defer关键字传入匿名函数
案例2
func main() {
startedAt := time.Now()
// defer + 匿名函数()
defer func() {
fmt.Println(time.Since(startedAt))
}()
time.Sleep(time.Second)
}
/*
$ go run main.go // 正确,符合预期
1s
*/
案例2
:虽然defer关键字使用值传递,但是因为拷贝的是函数指针,所以time.Since(startedAt)会在main函数返回前调用并打印出符合预期的结果
1.13. panic 宕机----程序终止运行
- 宕机:有些错误只能在运行时检查,如
数组访问越界、空指针引用
等,这些运行时错误会引起宕机(可能造成体验停止、服务中断) - 当宕机发生时
- 程序会中断运行,
- 立即执行在该 goroutine(可以先理解成线程)中被延迟的函数(defer 机制)
- 随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息,panic value 通常是某种错误信息
1.14. recover 宕机恢复----防止程序崩溃
说明:Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制。
- panic 和 recover 的关系
panic 和 recover 的组合有如下特性:
- 有 panic 没 recover,程序宕机。
- 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行
类比于try-catch机制
- panic:抛出错误
- recover:捕获错误,不会产生宕机,函数会继续执行
- 使用前提
- recover只在defer延时函数中生效(在正常执行成功,调用recover会返回nil,并没有其他任何效果)
- 使用场景
- 当 web 服务器遇到不可预料的严重问题时,在宕机崩溃前应该将所有的连接关闭(如果不做任何处理,会使得客户端一直处于等待状态)
- 使用案例
- 该函数传入一个匿名函数或闭包后的执行函数
- 当传入函数以任何形式发生 panic 崩溃后,可以将崩溃发生的错误打印出来
- 同时允许后面的代码继续运行,不会造成整个进程的崩溃
package main
import (
"fmt"
"runtime"
)
// 崩溃时需要传递的上下文信息
type panicContext struct {
function string // 所在函数
}
// 保护方式允许一个函数
func ProtectRun(entry func()) {
defer func() { // 延迟处理的函数
err := recover() // 发生宕机时, 由defer+recover捕获异常, 进行后面的处理
switch err.(type) {
case runtime.Error:
fmt.Println("runtime error:", err)
default:
fmt.Println("error:", err)
}
}()
entry()
}
func main() {
ProtectRun(func() {
fmt.Println("手动宕机前")
panic(&panicContext{"手动触发panic"}) // 手动触发宕机: 抛出异常, 后面的语句不会被执行
fmt.Println("手动宕机后") // 这句话不会被打印
})
}
/*
手动宕机前
error: &{手动触发panic}
*/
1.15. switch…case…
func sc() {
figer := 3
switch figer {
case 1, 2:
case 3:
default:
}
}
1.16. bool 布尔值
只有true、falase两个值
- Go语言与C语言不同,Go不允许将整形转换为bool
- 布尔型无法参与数据运算,也无法与其他类型进行转换
1.17. 流程控制: break / continue / goto / fallthrough
- break + 标签
func main() {
FOR: // 退出for循环
for i := 0; i < 10; i++ {
fmt.Println(i)
if i == 5 {
break FOR // break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块
// 标签要求必须定义在对应的for、switch和 select的代码块上
}
}
}
- fallthrough
func switchDemo(age int) {
switch {
case age < 25:
fmt.Println("switch1")
fallthrough // 仅仅向下走一层
case age > 25 && age < 35:
fmt.Println("switch2")
fallthrough
case age > 60:
fmt.Println("switch3")
default:
fmt.Println("switch4")
}
}
switchDemo(10)
/*
switch1
switch2
switch3
*/
2. 数据结构
2.1. 数组
- 相同类型、固定长度(一旦定义,长度不能变)
- 值传递 (数组作为参数时,是另一份拷贝)
预想修改数组的值,可以使用切片作为参数(切片是数组的一个引用)
package main
import "fmt"
// 数组-->值传递: 拷贝另外一份相同的副本, 原数组不会被修改
func modify_arr_1(arr [3]int, len int) {
for i := 0; i < len; i++ {
arr[i] = -1 // 修改数组失败, 因为是值传递
}
}
// 切片-->引用传递:形参\实参,指向同一份数据,一个修改,另外一个也会修改
func modify_arr_2(arr []int, len int) {
for i := 0; i < len; i++ {
arr[i] = -1 // 修改数组失败, 因为是值传递
}
}
func main() {
arr := [3]int{1,2,3}
fmt.Print(arr) // [1 2 3]
// 参数: 数组
modify_arr_1(arr, len(arr))
fmt.Print(arr) // [1 2 3]
// 参数: 引用slice
modify_arr_2(arr[:], len(arr))
fmt.Print(arr) // [-1 -1 -1]
}
- 声明
- var 数组变量名 [元素数量]Type
[10]int
[200]interface{}
- 初始化
var age [5]int{1,2,3}
var age = [5]int{1,2,3}
var age = [5]int{0:1, 2:3} // 指定下标
var age = [...]int{1,2,3}
- 比较两个数组是否相等
- 如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,我们可以直接通过较运算符(
==
和!=
)来判断两个数组是否相等
2.2. 切片
切片是对数组的一个连续片段的引用,所以切片是引用类型
切片比较
- 切片之间是不能比较的,不能使用
==
来判断两个切片是否含有全部相等的元素 - 切片唯一合法的比较操作:和nil比较 (一个nil值的切片,没有底层数组,一个nil值的切片长度和容量都是0。但是,不能说一个长度和容量都是0的切片一定是nil) ⇒ 要判断一个切片是否是空的,需要使用
len(slice)==0
,不应该使用slice==nil
- 声明
- var 切片变量名 []切片类型
[]int
[]interface{}
- 初始化
var slice []int = arr[start:end] // 从已知数组中切除[start,end)区间作为slice
slice []int = []int{1,2,3} // 使用字面量初始化新的切片
var slice []int = make([]int, len) // 使用关键字 make 创建切片
slice := make([]int, len)
slice := make([]int, len, cap)
说明:切片的初始化,相较于数组,有一个微笑的差别,就是[]中没有指明长度
- 数组:类型 [n]T 表示拥有 n 个 T 类型的值的数组
- 切片:类型 []T 表示一个元素类型为 T 的切片
- 切片
- 切片相关内置函数+操作函数
- 插入
- 注意:在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。
- 在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
/* 尾插 */
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
/* 头插 */
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
/* 中间插入 */
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
- 删除
- 删除开头的元素
// 方式1:通过直接移动数据指针
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
// 方式2:通过直接移动数据指针,即:也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
// 方式3:用 copy() 函数来删除开头的元素
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
- 从中间位置删除
对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
- 从尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
- 遍历:range迭代遍历
for idx, val := range slice {...}
易错使用点:range 返回的是每个元素的副本,而不是直接返回对该元素的引用,如下所示。
// 创建一个整型切片,并赋值
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
/*
Value: 变量值不一样 (value接收的是数据的拷贝)
Value-Addr: 用一个变量value接受,地址一样(都是value的地址)
ElemAddr: 用下标slice[index]接受,地址不一样(都是每个元素的地址)
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
*/
分析:value-Addr都是一样的(10500168),该值是一个新的拷贝,并不指向原来的元素地址slice[index] ==> 因此,要想获取每个元素的地址,需要使用&slice[index]
- 拷贝
格式:copy( destSlice, srcSlice []T) int
s1 := []int{1,2,3,4,5}
s2 := make([]int, 10)
copy(s2, s1)
- string与slice
- string底层就是一个byte的数组,因此,也可以进行切片操作
str := “hello world”
s1 := str[0:5]
fmt.Println(s1)
- 如何改变string中的字符值
str := “hello world” // 字符串
s := []byte(str) // 类型转换: string-->切片
s[0] = 'O' // 修改idx=0的值
str = string(s) // 类型转换: 切片-->string
- 数组arr、切片slice对比测试
func test_arr() {
var x[3]int = [3]int{1,2,3}
var y[3]int = x
fmt.Println(x,y) // [1 2 3] [1 2 3]
y[0]=999
fmt.Println(x,y) // [1 2 3] [999 2 3]
}
func test_slice() {
var x[]int = []int{1,2,3}
var y[]int = x
fmt.Println(x,y) // [1 2 3] [1 2 3]
y[0]=999
fmt.Println(x,y) // [999 2 3] [999 2 3]
}
2.3. map哈希表
结构体转map\string\interface{}的若干方法
重要: 结构体转map\string\interface{}的若干方法
- 格式:map[key]value
- 所有key数据类型相同;所有value数据类型形同
- 每个key在map中都是唯一的,且key必须支持
==
和!=
操作。 key的常用数据类型
- int、rune、string、指针、结构体(每个元素都支持
==
和!=
操作) - float32/64 类型从语法上可以作为key类型,但是实际一般不作为key,因为其类型有误差
注意:key与value可以有不同的数据类型 ==> 如果想不同,则使用interface作为value
- map基本操作
- 创建
// 1 字面值
{
m1 := map[string]string{
"m1": "v1", // 定义时指定的初始key/value, 后面可以继续添加
}
}
// 2 使用make函数
{
m2 := make(map[string]string) // 创建时,里面不含元素,元素都需要后续添加
m2["m2"] = "v2" // 添加元素
}
// 定义一个空的map
{
m3 := map[string]string{}
m4 := make(map[string]string)
}
- 增删改查
- 清空:有意思的是,Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map,不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
/* 增加 or 修改*/
m["age"] = 100
/* 查询 */
v := m["age"] // 从m中取键k对应的值给v,如果k在m中不存在,则将value类型的零值赋值给v
v, ok := m["age"] // 从m中取键k对应的值给v,如果k存在,ok=true,如果k不存在,将value类型的零值赋值给v同时ok=false
{
// 查1 - 元素不存在
v1 := m["x"]
v2, ok2 := m["x"]
fmt.Printf("%#v [%#v, %#v]\n", v1, v2, ok2) // 0 [0, false]
// 查2 - 元素存在
v3 := m["age"]
v4, ok4 := m["age"]
fmt.Printf("%#v [%#v, %#v]\n", v3, v4, ok4) // 100 [100, true]
}
/* 删除 */
delete(m, "age") // 若key不存在,不执行任何操作
- 遍历
/* 遍历
* 1) 遍历顺序是随机的
* 2) 使用for range遍历时,k/v使用的是同一块内存, 这也是容易出现错误的地方
*/
for k, v := range m {
fmt.Printf("k:[%v].v:[%v]\n", k, v)
}
- map的多键索引
- sync.Map(在并发环境中使用的map)
- Go语言中的 map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。
- 需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。
sync.Map 有以下特性:
- 无须初始化,直接声明即可。
- sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
- 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
补充说明:sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失,因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。
package main
import (
"fmt"
"sync"
)
func main() {
var scene sync.Map
// 将键值对保存到sync.Map:sync.Map 将键和值以 interface{} 类型进行保存
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)
// 从sync.Map中根据键取值: 将查询到键对应的值返回
fmt.Println(scene.Load("london"))
// 根据键删除对应的键值对: 使用指定的键将对应的键值对删除
scene.Delete("london")
// 遍历所有sync.Map中的键值对:Range() 方法可以遍历 sync.Map,
// 遍历需要提供一个匿名函数,参数为 k、v,类型为 interface{}
// 每次 Range() 在遍历一个元素时,都会调用这个匿名函数把结果返回
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
}
2.4. 链表list — container/list包
列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。
说明:① 列表与切片和 map 不同的是,列表并没有具体元素类型的限制,因此,列表的元素可以是任意类型,②这既带来了便利,也引来一些问题,例如给列表中放入了一个 interface{} 类型的值,取出值后,如果要将 interface{} 转换为其他类型将会发生宕机。
- 初始化列表:(1) New函数 (2) var关键字声明
变量名 := list.New()
var 变量名 list.List
- 插入元素
- 双链表支持从队列前方/后方插入元素,分别对应的方法是 PushFront 和 PushBack
l := list.New()
l.PushBack("fist")
l.PushFront(67)
方 法 | 功 能 |
InsertAfter(v interface {}, mark * Element) * Element | 在 mark 点之后插入元素,mark 点由其他插入函数提供 |
InsertBefore(v interface {}, mark * Element) *Element | 在 mark 点之前插入元素,mark 点由其他插入函数提供 |
PushBackList(other *List) | 添加 other 列表元素到尾部 |
PushFrontList(other *List) | 添加 other 列表元素到头部 |
- 删除元素
格式:链表对象.Remove(del_elem * Element) - 遍历链表
for i := list.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
2.5. string字符串
string 不能被修改,预想修改,需要先转换为 []byte 或 []rune
s1 := "hello world"
// 强制转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
- 常用API
• len / 拼接+ / strings.Split / strings.contains / strings.HasPrefix strings.HasSuffix / strings.Index() strings.LastIndex() / string.Join(a[] string, sep string)
func main() {
s1 := "D:\\Users\\cygwin" // 双引号
s2 := `D:\Users\cygwin` // 反引号 `` 反引号中所有的转义字符都失效,文本原样输出
s3 := `第一行
第二行
第三行
`
}
3. 结构体 / 函数 / 方法 / 接口interface
3.1. 结构体
- 定义结构体类型
type Student struct {
name_ string
age_ int
class_ string
}
- struct Tag 结构体标签
- 目的:结构体成员首字母小写对外不可见,但是若把首字母大写,这样与外界数据交互时会带来极大的不便 ⇒ 使用场景:json/sql/ini等
- 为结构体的成员添加说明,以便于使用==>这些说明可以通过反射获取到
type Student struct {
Name string "the name of student"
Age int "the age of student"
Class string "the class of student"
}
package main
import (
"encoding/json"
"fmt"
)
type person struct {
// Name必须是大写: 否则下面使用会编译报错
Name string `json:"name" db:"name" ini:"name"`
Age int `json:"age"`
}
func main() {
// 序列化: struct --> json
p1 := person{
Name: "Tom",
Age: 100,
}
b, err := json.Marshal(p1) // p1传给json包,p1中的成员属性首字母要大写
if err != nil {
fmt.Printf("Marshal failed, err:%v", err)
return
}
fmt.Printf("%#v\n", string(b))
// 反序列化: json --> struct
str := `{"name": "James", age: 30}`
var p2 person
json.Unmarshal([]byte(str), &p2)
fmt.Printf("%#v\n", p2)
}
- 匿名成员结构体(没有变量名的成员)
- 多用于临时场景
- 同一种类型的匿名成员,最多只允许存在一个
- 当匿名成员是结构体时,且两个结构体中都存在相同字段==>优先选择最近的字段
func main() {
var s struct { // 使用var定义匿名结构体,而不是使用type
x string
y int
}
s.x = "hello"
s.y = 100
}
type Person struct {
Name string
Age int
}
type Student struct {
Age int
Person //匿名内嵌结构体, 二者都有Age字段
}
func main() {
var stu = new(Student)
stu.Age = 34 //优先选择Student中的Age
fmt.Println(stu.Person.Age, stu.Age) // 0, 34
}
- 声明/初始化
// 非指针类型
var stu1 Student
// ins := &T{} 对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作
var stu2 *Student= &Student{}
stu2 := &Student{}
// new
var stu3 *Student = new(Student)
stu3 := new(Student)
(1) 键值对初始化结构体的书写格式
ins := 结构体类型名{
字段1: 字段1的值,
字段2: 字段2的值,
…
}
下面示例中描述了家里的人物关联,正如儿歌里唱的:“爸爸的爸爸是爷爷”,人物之间可以使用多级的 child 来描述和建立关联,使用键值对形式填充结构体的代码如下:
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
(3) 多个值列表初始化结构体的书写格式
- Go语言可以在“键值对”初始化的基础上忽略“键”,也就是说,可以使用多个值的列表初始化结构体的字段。
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段
- 每一个初始值的填充顺序必须与字段在结构体中的声明顺序一致
- 键值对与值列表的初始化形式不能混用
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
- struct没有构造函数,但是我们可以自定义构造函数(一般采用工厂模式自定义构造函数)
5.1. 普通的构造函数
func Newstu(name string, age int, class string) *Student { // 构造函数
return &Student{
name_ : name,
age_:age,
class_:class
}
}
func main() {
stu := Newstu("darren", 34, "math")
fmt.Println(stu.name)
}
5.2. 带有父子关系的结构体的构造和初始化——模拟父级构造调用
type Cat struct { /* 父类 */
color string
name string
}
type BlackCat struct { /* 子类 */
Cat // 嵌入Cat, 类似于派生
age int // 子类新添加的变量
}
// “构造基类”
func NewCat(name string) *Cat {
return &Cat{
name: name,
}
}
// “构造子类”
func NewBlackCat(name string, age int) *BlackCat {
cat := &BlackCat{}
cat.color = "black"
cat.name = name
cat.age = age
return cat
}
// 测试案例
func main() {
blk_cat := NewBlackCat("Tom", 11)
fmt.Println(blk_cat) // &{{black Tom} 11}
}
3.2. 函数:Golang 一等公民
func 函数名(参数列表) (返回值列表)
{ }
func add(a, b int) int {
return a + b;
}
func modify(a int) { // 无返回值, 返回值列表不用写
a = 100
}
// 命令的返回值,就相当于在函数中声明一个变量
func f(x, y int) (ret int) {
ret = x + y
return // 默认返回的是ret
}
- 内置函数
close\len\new\make\append
panic/recover - 匿名函数
- 匿名函数的定义:就是没有名字的普通函数定义
func(参数列表)(返回参数列表){
函数体
}
- 在定义时调用匿名函数
func(data int) {
fmt.Println("hello", data)
}(100)
- 将匿名函数赋值给变量
// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)
3.3. 闭包(匿名函数):一个函数与其相关的引用函数组合而成的实体
- Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,
即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量
- 被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应
- 函数 + 引用环境 = 闭包,闭包是一个函数,该函数包含了它外部作用域的一个变量
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber1 为一个函数指针,函数 i 为 0 */
nextNumber1 := getSequence()
/* 调用 nextNumber1 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber1()) // 函数指针,能够保存返回值结果
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
/* 创建新的函数 nextNumber2 ,并查看结果 */
nextNumber2 := getSequence()
fmt.Println(nextNumber2())
fmt.Println(nextNumber2())
}
/*
1
2
3
1
2
*/
- 可变参数
func add(arg ...int) int { // arg是一个slice
}
3.4. 方法 / 接收器
格式:func (recv_name 接受者类型)函数名(参数列表) (返回值列表) { }
① 接收器变量命名:接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母,而不是 self、this 之类的命名
② 使用格式:方法只能被 “接收者对象” 调用
type Person struct {
Name string
Age int
}
/*
* 方法: 与Person结构体绑定, 类似于OOP中的类
* @param [in] p 接收者: p代表结构体本身的实列,类似python中的self,这里p可以写为self
*/
func (p Person) Getname() string {
fmt.Println(p.Name)
return p.Name
}
func main() {
var person1 = new(Person)
person1.Age = 34
person1.Name = "darren"
person1.Getname()
}
简单的理解:golang中的(一个类型 + 方法) = C++中的类
- 定义:方法是作用在接收者(个人理解成作用对象)上的一个函数,其中,接收者是某种类型的变量
接收者的类型
- 不能是一个接口类型,因为接口是一个抽象的定义,方法是一个具体的实现(编译错误
invalid receiver type…
) - 可以是自定义结构体类型
- 可以是Golang的基本类型(int / bool / string…)
- 可以是数组的别名类型
- 甚至可以是函数类型
- ”一个类型+方法 “ 等价于 ”面向对象中的一个类“
一个重要的区别:
- 在Golang中,类型的代码和绑定在它上面的方法的代码可以不放在一起,它们可以存在不同的源文件,唯一的要求是:它们必须是同一个包的。(接收者、方法必须在同一个包内)
- 注意事项
- 方法是一种特殊的函数,因此Golang中的方法不能进行重载(即:对于一个类型,只能有一个给定名的方法)
- 接收器类型(指针*T / 非指针T)
- 理解指针类型的接收器
- 接收者对象是 “指针”时,更接近于面向对象中的 this 或者 self
- 由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的
- 理解非指针类型的接收器
- 接收者对象是 “值”时,这个值是类型实例的拷贝,无法改变接收者的值
- 在非指针接收器的方法中可以获取接收器的成员值,但修改后无效
总结:指针/非指针接收器的使用
- 小对象由于复制速度较快 ==> 非指针接收器
- 大对象由于复制性能较低 ==> 指针接收器
- 继承 ---- 结构体内嵌模拟类的继承
package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
fmt.Println("can calk")
}
// 人类
type Human struct {
Walkable // 内嵌行走结构体: 继承了行走特性
}
// 鸟类
type Bird struct {
Walkable // 鸟类能行走
Flying // 鸟类能飞行
}
func main() {
// 实例化鸟类
b := new(Bird)
b.Fly()
b.Walk()
// 实例化人类
h := new(Human)
h.Walk()
}
3.5. 接口interface
- 定义:① interface是一组方法定义的集合,这些方法无需实现 ② interface中不能包含任何变量
- interface是方法的集合
- interface是一种类型,并且是指针类型
- interface更重要的作用是:多态的实现
- 当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用
type 接口名称 interface {
method1 (参数列表) 返回值列表
method2 (参数列表) 返回值列表
...
}
// 1-接口定义
type Skills interface {
Running() // 包含多个函数的声明, 无需实现函数
Getname() string
}
// 2-结构体定义:用于实现接口Skills
type Student struct {
Name string
Age int
}
// 3-实现“方法”(连接“接口”与“结构体”之间的桥梁):实现Skills接口的函数, 接收器类型为Student
func (p Student) Getname() string { //实现Getname方法
fmt.Println(p.Name)
return p.Name
}
func (p Student) Running() { // 实现 Running方法
fmt.Printf("%s running", p.Name)
}
// 4-使用接口
func main() {
var skill Skills // 4.1-定义接口对象
stu1 := Student{ // 4.2-定义结构体对象
Name : "darren",
Age : 34
}
skill = stu1 // 4.3-将结构体对象, 强制转换为接口对象
skill.Running() // 4.4-使用“接口对象”调用接口, 输出结果: darren running
}
- 接口嵌套
- 接口嵌套,可以理解为继承 ==> 子接口拥有父接口的所有方法
- 若使用该子接口,必须将父接口和子接口的所有方法都实现
type Skills interface {
Running()
Getname() string
}
type Test interface {
sleeping()
Skills //继承Skills
}
- 接口多态
- 接口是实现多态的利器:同一个接口interface,不同结构体类型实现,且不同结构体对象都能执行调用
// 2.4 interface多态
package main
import "fmt"
type Skills interface {
Running()
Getname() string
}
type Student struct {
Name string
Age int
}
type Teacher struct {
Name string
Salary int
}
func (p Student) Getname() string { //实现Getname方法
fmt.Println(p.Name)
return p.Name
}
func (p Student) Running() { // 实现 Running方法
fmt.Printf("%s running", p.Name)
}
func (p Teacher) Getname() string { //实现Getname方法
fmt.Println(p.Name)
return p.Name
}
func (p Teacher) Running() { // 实现 Running方法
fmt.Printf("\n%s running", p.Name)
}
func main() {
var skill Skills // Student和Teacher都实现了各自的接口类Skills
stu := Student{"Student", 18}
tcr := Teacher{"Teacher", 30}
// 调用前, 先给skill对象赋值; 之后, 再使用skill对象调用接口
skill = stu
skill.Running() // Student running
skill = tcr
skill.Running() // Teacher running
}
- 方法集与方法调用问题两个结论:
T
与*T
(1) 对于T
类型,它的方法集只包含接收者类型是T
的方法 ==> 否则,会编译错误
(2) 对于*T
类型,它的方法集则包含接收者为T
和*T
类型的方法,也就是全部方法 - 类型与接口的关系在Go语言中类型和接口之间有一对多和多对一的关系,即:
(1) 一个类型可以实现多个接口
(2) 多个类型可以实现相同的接口 - 空接口类型 interface{}
3.6. 空接口interface{}
接口中没有任何方法,就叫空接口。
- 任意结构体都隐式的实现了空接口
- 空接口可以保存任何值(类比于C++中的万能指针),也可以从空接口中取出原值(空接口可以保存任何类型这个特性可以方便地用于容器的设计)
- 空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。==> 因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口
因为空接口可以存储任意类型的值,所以空接口在Go语言中的使用十分广泛!
(1) 将值保存在空接口
var any interface{}
any = 1
any = "hello"
any = false
(2) 从空接口中获取值 ==> 类型断言
语法格式:
/*
* x: 表示类型为interface{}的变量
* T: 表示断言x可能是的类型
*
* v: x转换为T类型后的变量
* ok:断言成功/失败
*/
v, ok = x.(T)
空接口使用: switch…case…
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
case int:
default:
}
}
// 声明a变量, 类型int, 初始值为1
var a int = 1
// 声明i变量, 类型为interface{}, 初始值为a, 此时i的值变为1
var i interface{} = a
// 声明b变量, 尝试赋值i
var b int = i // ==> 编译失败
// 解决编译失败的问题,使用类型断言的方式
var b int = i.(int)
编译失败::cannot use i (type interface {}) as type int in assignment: need type assertion
,即编译器告诉我们,不能将i变量视为int类型赋值给b
(3) 空接口的值比较
空接口在保存不同的值后,可以和其他变量值一样使用==
进行比较操作。空接口的比较有以下几种特性。
3-1. 类型不同的空接口间的比较 ==> 结果不相同
3-2. 不能比较空接口中的动态值
- 当接口中保存有动态类型的值时,运行时将触发错误,代码如下:
// c保存包含10的整型切片
var c interface{} = []int{10}
// d保存包含20的整型切片
var d interface{} = []int{20}
// 这里会发生崩溃
fmt.Println(c == d)
(4) 空接口实现可以保存任意值的字典
4. 编码技巧
4.1. 你需要知道的那些go语言json技巧
4.2. 结构体转map\string\interface{}的若干方法
4.3. Go语言中的单例模式
4.4. 切片splice使用技巧