Go 语言基础
Go 语言
2009年诞生,Go/Golang 是云计算时代的 C 语言。专门对多处理器系统应用的编程做了优化。媲美 C/C++ 的速度,更加安全,支持并行进程。
部署简单:可直接编译成机器码,不依赖其他库,所以部署相对简单。
动态效率:静态语言,可以编译时检查出错误,虽然是静态语言但是写起来效率很高。Python 是动态语言,写效率高,但是性能比 Go 稍差。
并发:语言层面支持并发,是 Go 最大的特色,天生支持并发。
GC:内置 runtime ,支持垃圾回收(GC,garbage collection),属于动态语言特性之一吧。动态分配空间,但是不需要手动释放。
用途:网络编程(Web/API 应用)、服务器编程(处理日志、数据打包、虚拟机处理、文件系统)、分布式系统、内存数据库(缓存)、云平台
安装 Go 环境,配置环境变量,并安装 IDE
参考:https://blog.csdn.net/w616589292/article/details/50824297
常用的命令:
go version:查看版本
go env:环境变量(根目录、编译器等配置)
go build xx.go:编译 Go 代码,生成一个可执行程序
go run 程序:运行可执行程序
go run xx.go:不生成程序,直接运行
运行:文件以 go 为后缀,go file.go 运行 go 程序
编码:UTF-8
学习资料:
Go 官网:https://golang.org/
Go 中文社区:https://studygolang.com
Go 中文在线文档(不错,API文档):https://studygolang.com/pkgdoc
注意:
- 左括号必须与函数名同行
- Println 会自动换行
- 调用函数都需要导入包
- go 语言以包作为管理单位,每个文件要先声明包,程序必须要有一个 main 包
- 语句结尾不需要分号
- 保存后会自动格式化
- 一个工程(文件夹)只能有一个 main 函数
- 导入包后必须要使用
基本数据类型、流程控制
常量和变量
1、数据类型的作用:告诉编译器这个变量应该以多大的内存存储
2、变量命名规范:
- 字母、下划线、数字
- 不能以数字开头
- 名字不能关键字
- 区分大小写
3、变量声明格式
格式:var 变量名 类型
var a int
注意:
- 变量声明后必须要使用
- 只声明没有初始化的变量,默认值是 0
- 同一个花括号 {} 内的变量名是唯一的
4、变量的赋值
a = 10
赋值前必须要声明变量
5、变量的初始化
声明变量的同时初始化
var b int = 10
6、自动推导类型
必须初始化,通过初始化的值确定类型
c := 30
fmt.Printf("c type is %T\n", c)
%T:打印变量所属的类型
:=:自动推到类型(先声明类型,再赋值)
7、Println 和 Printf 区别
// 一段一段处理,自动加换行
fmt.Println("a = ", a)
// 格式化输出
fmt.Printf("a = %d\n", a)
8、多重赋值
i, j := 10, 20
i, j = j, i // 交换 2 个变量的值
9、匿名变量
_ 表示匿名变量,丢弃数据不处理
tmp, _ = i, j
匿名变量配合函数返回值使用才有优势
func toast() (a, b, c int) {
}
var c, d int
c, _, d = toast()
10、常量
声明用 const
const a int = 10
常量的自动类型推导不需要 :=,用 = 即可
const b = 11.2
11、多个变量或常量的初始化
用 () 扩起来即可
// var a int
// var b float64
等价于
var (
a int = 1
b float64 = 2.0
)
也等价于自动推导类型
var (
a = 1
b = 2.0
)
// const i = 10
// const j = 20
等价于
const (
i int = 10
j float64 = 3.14
)
也等价于自动推导类型
const (
i = 10
j = 3.14
)
12、iota 枚举
iota:
- 给常量赋值使用,每个一行累加 1,同行值相同
- 遇到 const 重置为 0
- 可以只写一个 iota
const (
a = iota // 0
b = iota // 1
c = iota // 2
)
const d = iota // 0
const (
a1 = iota // 0
b1, b2 // 1
c1 // 2
)
13、类型的分类
bool 布尔类型,1
byte 字节型,1
rune 字符类型,4
int, uint 整型,4(32bit)/8(64bit)
float32 浮点型, 4,小数位精确到 7 位
float64 浮点型,8,小数位精确到 15 位
string 字符串型
// bool
a := true
// float64
b := 3.14
// byte
var ch byte
ch = 97
fmt.Printf("%c", ch) // a
// string
str := "abc"
fmt.Println("len(str) = ", len(str)) // 3
14、字符与字符串区别
字符:
- 单引号
- 往往都是一个字符,转义字符除外 '\n'
ch := 'a'
字符串:
- 双引号
- 字符串有1个或多个字符组成
- 字符串都是隐藏了一个结束符,'\0'
str = "abcd"
// 操作字符串的某个字符,从 0 开始操作
fmt.Printf("str[0] = %c, str[1] = %c\n", str[0], str[1])
15、复数类型
实部 + 虚部
var t complex123
t = 2.1 + 3.14i
fmt.Println("t = ", t)
// 自动推导类型
t2 := 3.3 + 4.4i
// 通过内建函数,获取实部和虚部
fmt.Println("real(t2) = ", real(t2), ", imag(t2) = ", imag(t2))
16、格式化输出
%d: 整型
%s: 字符串
%c: 字符
%f: 浮点型
%T:获取类型
%v: 自动匹配格式输出(重要)
17、输入的使用
var a int
// 阻塞等待用户的输入
fmt.Scanf("%d", &a)
18、类型转换
bool 与 int 互相转换
字符型可以与 int 互相转换
ch := 'a'
var t int = int(ch)
fmt.Println("t = ", t)
19、类型别名
// int64 类型别名改为 bigint
type bigint int64
var a bigint // 等价于 var a int64
// 批量起别名
type (
long int64
char byte
)
20、运算符
算术运算符:+、-、*、/、%、++(只有后自增,没有前自增,a++)、--(只有后自减,没有前自建,b--)
关系运算符:==、!=、>、<、>=、<=
逻辑运算符:!、&&、||
位运算符: &、|、^、<<、>>
赋值运算符: =、+=、-=、<<=、>>=、&=、^=、|=
其他运算符:&(取地址)、*(取值)
运算符优先级
一元运算符拥有最高的优先级,二元运算符的运算方向是从左到右
第 7 级:^ !
第 6 级:* / % << >> & &^
第 5 级:+ - | ^
第 4 级:== != < <= >= >
第 3 级:<_
第 2 级:&&
第 1 级:||
21、选择语句
if 语句
s := "xiao"
if s == "xiao" { // 左括号和 if 在同一行
// do something
}
// if 支持1个初始化语句,初始化语句和判断条件以分号分隔
if a := 10; a == 10 {
// do something
}
a = 20
if a == 10 {
// do something
} else if a > 10 {
// do something
} else {
// do something
}
Switch 语句
num := 1
switch num { // switch 后面写的是变量本身
case 1:
// do something
break // go 语言保留 break 关键字,写不写都行,默认包含 break
case 2:
// do something
fallthrough // 不跳出,贯穿执行
default:
// other
}
// switch 也支持一个初始化语句,用分号分隔
switch a := 1; a {
case 1:
// do something
}
// case 后面可以放条件
switch b := 1; b {
case b > 1:
// do something
}
// case 后面可以放多个
switch c := 1; c {
case 1, 2 , 3:
// do something
}
22、循环语句
只有 for 循环和 range 迭代器,没有 while、do while
for 循环
for i := 1; i <= 100; i++ {
// 循环
}
range 迭代(配合数组、切片使用)
str := "abc"
// range 迭代打印每个元素,默认返回2个值:一个是元素的位置,一个是元素本身
for i, data := range str {
fmt.Printf("str[%d]=%c\n", i, data)
}
// 如果不要元素本身,可以使用匿名变量
for i, _ := range str {
fmt.Printf("%d", i)
}
23、跳转语句
break: 跳出最近的循环,只能用在 loop、switch、select 语句
continue: 跳过本次循环,只能用在 loop 语句
goto: 可以用在任何地方,但是不能垮函数使用(影响阅读,比较少用)
a := 1
goto End // 跳转到标签
End: // 标签定义
// something
函数、工程管理
1、函数定义格式
函数可以不分顺序,但是一定要调用实现的函数
定义函数时,在函数名后面 () 定义的参数叫形参
参数传递,只能由实参传递给形参,不能反过来,单向传递
func FuncName(/*参数列表*/)(o1 type1, o2 type2) {
// 函数体
return v1, v2; // 返回多个值
}
// 无参无返回值
func myFunc() {
// do something
}
// 有参无返回值
func myFunc1(a, b int) {
// do something
}
func myFunc2(a string, b int) {
// do something
}
// 不定参数
// ...int 这样的类型,...type 不定参数类型,不定参数一定是最后一个参数。固定参数一定要传参数,不定参数根据需求传递
func myFunc3(args ...int) {
fmt.Println("len(args) = ", len(args)) // 获取用户传递参数的个数
}
// 不定参数的传递
func myfunc(tmp ...int) {
for _, data := range tmp {
fmt.Println("data =", data)
}
}
func test (args ...int) {
// 全部元素传递
myfunc(args...)
// args[0]~args[1] 传递过去
myfunc(args[:2]...)
// args[2]~args[len - 1] 传递过去
myfunc(args[2:]...)
}
// 多个返回值
func myfunc3() (int, int, int) {
return 1, 2, 3
}
或用官方推荐的
func myfunc4() (a int, b int, c int) {
a, b, c = 111, 222, 333
return
}
// 有参数有返回值
func myfunc5(a int, b int)(max, min int) {
// do something
}
2、函数调用流程
函数调用流程:先调用后返回,先进后出
函数递归:函数调用自己本身,利用此特点
3、函数类型
在 go 语言中,函数也是一种数据类型,我们可以用 type 来定义它,它的类型就是所有相同的参数,相同返回值的一种类型。(类似 C 语言中的函数指针)
func add(a, b int) int {
return a + b
}
// 没有函数名字,没有{}, funcType 是一个函数类型
type funcType func(int, int) int
func main() {
var fTest funcType
fTest = add // 是变量就可以赋值
result := fTest(10, 20) // 等价于 add(10, 20)
}
4、回调函数
回调函数:函数有一个参数是函数类型,这个函数就是回调函数
type funcType func(int, int) int
// 回调函数
// 计算器,可以进行四则运算
// 多态,多种形态,调用同一个接口,不同的表现。(先有想法,然后实现功能)
func Calc(a, b int, fTest funcType)(result int) {
fmt.Println("Calc")
result = fTest(a, b)
return
}
func add(a, b int) int {
return a + b
}
func main() {
a := Calc(1, 1, add)
fmt.Println("a = ", a)
}
5、匿名函数与闭包
闭包:函数“捕获”了和它在同一作用于的其他常量和变量,这就意味着当闭包被调用时候,不管在程序什么地方调用,闭包都能够使用这些常量和变量。(它不关心这些捕获的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还存在)
在 go 语言中,所有的匿名函数(go 语言规范中成为函数字面量)都是闭包。匿名函数是指不需要定义函数名的一种函数实现方式。
func main() {
a := 10
str := "mike"
// 匿名函数,没有函数名字,函数定义,还没有调用
f1 := func() {
fmt.Println("a = ", a)
fmt.Println("str = ", str)
}
f1()
// 匿名函数的定义和调用
func() {
fmt.Printf("abc")
} () // 后面的 () 代表调用此匿名函数
// 匿名函数,有参数有返回值
x, y := func(i, h int)(max, min int) {
if i > j {
max = I
min = j
} else {
max = j
min = j
}
return
} (10, 20)
}
6、闭包捕获外部变量的特点
func main() {
a := 10
str := "mike"
func() {
// 闭包以引用方式捕获外部变量,里面改了,外面也会修改,改的是同一个变量
a = 666
str = "go"
} ()
t1 := test01() // 1
t2 := test01() // 1
t3 := test01() // 1
// 它不关心这些捕获的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还存在
s1 := test02() // 1
s2 := test02() // 4
s3 := test02() // 9
}
func test01() int {
// 函数被调用时,x 才分配空间,才初始化为0
var x int // 没有初始化,值为 0
x++
return x * x // 函数调用完毕,x 自动释放
}
// 函数的返回值是一个匿名函数,返回一个函数类型
func test02() func() int {
var x int
return func() int {
x++
return x * x
}
}
7、defer(中文:延迟,推迟)
用于延迟一个函数或方法(或者当前所创建的匿名函数)的执行。(类似面向对象语言析构函数的作用)
主要是用于函数结束前做一些清理工作
注意:
- defer 语句只能出现子啊函数或方法的内部
- 多个 defer 语句,按照 LIFO(先进后出)的顺序执行。(哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行)
func main() {
defer fmt.Println("bbb") // 最后打印,main函数结束前才调用
fmt.Println("aaa")
}
8、局部变量的特点
定义在 {} 里面的变量就是局部变量,只能在 {} 里面有效。
执行到定义变量的那句话,才开始分配空间,离开作用域自动释放
作用域:变量作用的范围
9、全局变量
定义在函数外部的变量是全局变量
全局变量在任何地方都能使用
注意:
- 不同作用域,允许定义同名变量
- 使用变量的原则,就近原则(没有局部变量,才用全局变量)
10、工作区
Go 代码必须放在工作区中。工作区就是一个对应于特定工程的目录,它包含 3 个子目录:
- src:用于以代码包的形式组织并保存 Go 源码(必须的)
- pkg:用于存放由 go install 命令构建安装后的代码包的“.a”归档文件
- bin:与 pkg 类似,在通过 go install 命令完成安装后,保存由 Go 命令远吗文件生成的可执行文件
src 目录用于包含所有的源代码,是 Go 命令行工具一个强制的规则,而 pkg 和 bin 则无需手动创建,如果必要 Go 命令行工具在构建过程中会自动创建这些目录。
注意:
- 只有当环境变量 GOPATH 中包含一个工作区的目录路径时,go install 命令才会把命令源码安装在当前工作区的 bin 目录下。
- 如果环境变量 GOPATH 中包含多个工作区的目录路径,像这样执行 go install 命令就会失效,此时必须设置环境变量 GOBIN
11、GOPATH 设置
为了能够构建这个工程,需要先把所需工程的根目录加入到环境变量 GOPATH 中,否则,即使处于同一目录(工作区),代码之间也无法通过绝对代码包路径完成调用。
实际开发中,工作目录往往有多个。这些工作目录的目录路径都需要添加至 GOPATH。当有多个目录时,请注意分隔符,多个目录是,linux 系统是冒号,当有多个 GOPATH 时,默认会将 go get 的内容放在第一个目录下。
12、包
所有 Go 语言的程序都会组织成若干组文件,每组文件都被称为一个包。这样每个包的代码都可以作为很小的复用但愿,被其他项目引用。
一个包的源代码保存在一个或多个以 .go 为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径。
导入包方式:
import "fmt" // 格式化输出
import "os" // 系统方法
// 点操作,调用函数,无需通过包名(不推荐)
import . "fmt"
// 给包名起别名
import io "fmt"
// 忽略此包(可以不用,不影响编译,但是会调用包里面的 init 函数)
import _ "fmt"
// 多个包的导入方式
import (
"fmt"
"os"
)
注意:
- 导入包必须使用,否则编译失败
13、自定义包
对于一个较大的应用程序,我们应该将它的功能性分隔成逻辑的单元,分别在不同的包里实现。我们创建的自定义包最好放在 GOPATH 的 src 目录下(或者 GOPATH src 的某个子目录)
在 Go 语言中,代码包中的源码文件名可以是任意的。但是,这些任意名称的源码文件都必须以包声明语句作为文件中的第一行,每个包都对应于一个独立的名字空间:
package calc
包中成员以名称首字母大小写决定访问权限:
- public: 首字母大写,可被包外访问
- private: 首字母小写,仅包内成员可以访问
14、包的函数调用
同一目录:
- 同一个目录下不能定义不同的 package
- 同一个目录,可以直接调用别的文件的函数
不同目录:
- 包名不一样
- 调用别的包的函数,这个包函数名字如果是小写,无法让别人调用,要想让别人调用,必须首字母大写
- 调用不同包里面的函数, 导入包, 格式: 包名.函数名()
注意:如果导入一个包,会先执行包的 init 函数
15、main 函数和 init 函数
Go 里面有两个保留的函数:init 函数(能够应用于所有的 package)和 main 函数(只能应用于 package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个 package 里面可以写任意多个 init 函数,但这无论是对于可读性还是后期维护性来说,我们都强烈建议用户在一个 package 中每个文件只写一个 init 函数。
Go 程序会自动调用 init() 和 main(),所以你不需要在任何地方调用这两个函数。每个 package 中的 init 函数都是可选的,但是 package main 就必须包含一个 main 函数。
每个包可以包含任意多个 init 函数,这些函数都会在程序执行开始的时候被调用。所有被编译器发现的 init 函数都会安排在 main 函数之前执行。init 函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作。
程序的初始化和执行都始于 main 包。如果 main 包还导入了其他的包,那么就会在编译时将它们依次倒入。有时一个包会被多个包同时导入,那么它就会被导入一次(例如很多包可能都会用到 fmt 包,但是它只会被导入一次,因为没有必要导入多次)。
到一个包被导入时,如果该包还导入了其他的包,那么会将其他的包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完成了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。
image.png16、go install
设置环境变量 GOBIN
go install 生成 bin 和 pkg 目录
复合类型
1、复合类型分类
pointer 指针
array 数组(默认是 0)
slice 切片(引用类型)
map 字典(引用类型)
struct 结构体
2、指针
指针是一个代表某个内存地址的值。
每个变量有 2 层含义:变量的内存(内容)、变量的地址
Go 语言虽然保留了指针,但与其他编程语言不同的是:
- 默认值是 nil,没有 NULL 常量(C 语言如果没有指向,则为 NULL,Go 语言没有指向是为 nil)
- 操作符 & 取变量地址, * 通过指针访问目标对象
- 不支持指针运算,不支持 -> 运算符,直接用 . 访问目标成员
var a int = 10
// 保存某个变量的地址,需要指针类型, *int 保存 int 的地址,**int 保存 *int 的地址
// 声明(定义),定义只是特殊的声明
// 定义一个变量 p,类型为 *int
var p *int
p = &a // 指针变量指向谁,就把谁的地址赋值给指针变量
*p = 6 // *p 操作的是 p 所指向的内存(相当于 a = 6)
3、new 函数
表达式 new(T) 将创建一个 T 类型的匿名变量,所做的是为 T 类型的新值分配并清零一块内存空间,然后将这块内存空间的地址作为结果返回,而这个结果就是指向这个新的 T 类型值的指针值,返回的指针类型为 *T
我们只需要使用 new() 函数,无需担心共内存的生命周期或怎样将其删除,因为 Go 语言的内存管理系统会帮我们打理一切。
就是动态分配空间,不用关心回收,因为 GC 会自动处理
var p *int
// new(int)会开辟一个 int 型的空间,并把地址给 p
p = new(int)
4、值传递、地址传递
a, b := 10, 20
// 值传递
swap(a, b)
// 地址传递
swap(&a, &b)
5、数组
// 数组定义
var array [50]int
// 二维数组的定义
var array [10][50]int
// 二维数组的初始化(或者遍历赋值),如果部分初始化,没有初始化的值为 0
b := [3][4]int {{1, 2, 3, 4}, {5, 6, 7, 7}, {9, 10, 11, 12}}
// 操作数组
for i := 0; i < len(array); i++ {
array[i] = i + 1
}
// 数组的比较(比较每个元素是否相同)
a := [5]int{1, 2, 3, 4, 5}
b := [5]int{1, 2, 3, 4, 5}
fmt:Println("a == b ", a == b)
6、随机数
import "math/rand"
import "time"
fucn main() {
// 设置种子,只需要一次
// 如果种子参数一样,每次运行程序产生的随机数都一样
rand.Seed(time.Now().UnixNano()) // 以当前系统时间为种子参数
for i := 0; i < 5; i++ {
// 产生随机数
fmt.Println("rand = ", rand.Int())
// 限制范围,如下限制在 100 以内
fmt.Println("rand = ", rand.Int(100))
}
}
7、数组、数组指针作为函数参数
a := [5]int{1, 2, 3, 4, 5}
// 数组传递过去,是值传递(copy一份到形参)
modify(a)
// 数组指针传递过去,是引用传递/地址传递
modify(&a)
8、切片(slice)
数组的长度在定义之后无法再次修改,数组是值类型,每次传递都将产生一份副本。Go 语言提供了数组切片来弥补数组的不足。
切片并不是数组或数组指针,它通过内部指针和相关属性引用数组片段,以实现变长方案。
slice 并不是真正意义的动态数组,而是一个引用类型,slice 总是指向一个底层 array,slice 的声明也可以像 array 一样,只是不需要长度。
[low:high:max]
low: 下标的起点
high:下标的终点(不包括此下标)
cap: max - low, 容量
// 数组: 不能修改长度,len、cap 不变
a := [5]int{}
// 切片([]里面为空或者...):可以修改长度
s := []int{}
// 给切片默认追加一个成员
s = append(s, 11)
array := []int{0, 1, 2 ,3, 4}
s1 := array[:] // 等价于 [0:len(array):len(array)]
s2 := array[:2] // 从 0 开始,取 2 个,容量也是 2
s2 := array[3:] // 从下标 3 开始,到结尾
9、切片的操作
append: 切片增加数据,如果超过原来的容量,通常以2倍容量扩容
s1 := []int{}
// 在原切片的末尾添加元素
s1 = append(s1, 1, 2)
10、copy
将源的位置 copy 到目的位置
srcSlice := []int{1, 2}
destSlice := []int{6, 6, 6, 6, 6, 6}
copy(destSlice, srcSlice) // destSlice 将变为 {1, 2, 6, 6, 6, 6}
11、map
Go 语言中的 map(映射、字典)是一种内置的数据结构,它是一个无序的 key-value 对的集合。
map 的格式为: map[keyType]valueType
对于 map,只有 len,没有 cap
map 做函数参数,是引用传递
info := map[int]string {
110 : "mike",
111 : "yoyo"
}
// 通过 make 创建,可以指定长度,只是指定了容量,里面没有数据
m2 := make(map[int]string, 10)
m2[1] = "mike"
// 初始化
m3 := map[int]string{ 1 : "mike"}
// 遍历:第一个返回值是 key,第二个返回值是 value,遍历结果是无序的
for key, value := range m3 {
// do something
}
// 第一个返回值是 key 对应的 value, 第二个返回值是 key 是否存在的条件
value, exist := m[1]
// 删除 key 值
delete(m, 1) // 删除 key 为 1 的内容
12、结构体
结构体是一种聚合的数据类型,它是由一系列具有相同类型或者不同类型的数据构成的数据集合,每个数据称为结构体的成员
// 结构体定义
type Student struct {
id int
name string
sex byte
age int
addr string
}
func main() {
// 顺序初始化,每个成员都必须初始化
s1 := Student{1, "mike", 'm', 18, "add"}
// 指定成员初始化,没有初始化的自动为 0
s2 := Student{name : "mike"}
// 结构体指针变量
p1 := &s2
// 操作成员,需要 . 运算符
s1.id = 1
s1.name = "mi"
// 也可以用指针操作成员
p1.id = 2
(*p1).id = 3
// 通过 new 申请一个结构体
p2 := new(Student)
// 值传递
test(s1)
// 地址传递/引用传递
test(*s1)
}
13、可见性
Go 语言对关键字的增加非常吝啬,其中没有 private、protected、public 这样的关键字。要使某个富豪对其他包(package)可见(即可以访问),需要将该符号定义为以大写字母开头。
如果想使用别的包的函数、结构体类型、结构体成员、函数名、类型名、结构体成员变量名,首字母必须大写,可见
如果首字母小写,只能在同一个包里使用
需要设置环境变量
package main // 必须有一个 main 包
import "test"
func main() {
// 包名.函数名
}
面向对象编程
1、概述
Go 语言并不支持继承、虚函数、构造函数、析构函数、隐藏的 this 指针等。
尽管 Go 语言中没有封装、继承、多态这些概念,但同样通过别的方式实现这些特性:
- 封装:通过方法实现
- 继承:通过匿名字段实现
- 多态:通过接口实现
2、匿名字段
type Person struct {
name string
sex byte
}
type Student struct {
Person // 只有类型,没有名字,匿名字段,继承了 Person 的成员
add string
}
type Student1 struct {
Person
int // 基础类型的匿名字段
}
func main() {
s1 := Student(Person("mike", 11), "beijing")
// 赋值
s1.Person = Person("mike", 10)
// 也可以直接访问
s1.name = "yoyo"
// 可以直接操作基础类型的匿名字段
s2 := Student(Person("mike", 11), "beijing")
s2.int = 10
}
3、结构体指针匿名字段
type Person struct {
name string
sex byte
}
type Student struct {
*Person // 指针类型
add string
}
func main() {
// 第一种直接初始化
s1 := Student{&Person{"mike", 'm', 18}, 666, "bj"}
// 第二种:先定义变量
s2 := new(Person)
s2.name = "yoyo" // 然后赋值
}
4、方法
在 Go 语言中,可以给人以自定义类型(包括内置类型,但不包括指针类型)添加相应的方法。
方法总是绑定对象实例,并隐式将实例作为第一实参(receiver),方法的语法如下:
func(receiver ReceiverType) funcName (parameters)(results)
参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。
参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
不支持重载方法,也就是说,不能定义名字相同但是参数不同的方法。
只要接收者类型不一样,这个方法就算重名,也是不同的方法,不会出现重复定义函数的错误。
// 面向过程
func Add01(a, b int) int {
return a + b
}
// 面向对象,方法:给某个类型绑定一个函数
type long int
// tmp 叫接收者,接收者就是传递的一个参数
func (tmp long) Add02(other long) long {
return tmp + other
}
func main() {
var result int
result := Add01(1, 1) // 普通函数调用方式
fmt.Println(result = ", result)
// 定义一个变量
var a long
// 调用方法格式:变量名.函数(所需参数)
r = a.Add02(3)
}
type Person struct {
name string
sex byte
}
// 带有接收者的函数叫方法
func (tmp Person) PrintInfo() {
fmt.Println("tmp = ", tmp)
}
// 通过一个函数,给成员赋值
func (p *Person) SetInfo(n string, s byte, a int) {
p.name = n
p.sex = s
p.age = a
}
func main() {
// 定义同时初始化
p := Person{"mike", 'm', 18}
p.PrintInfo()
// 定义一个结构体变量
var p2 Person
(&p2).SetInfo("yoyo", 'f', 22)
p2.PrintInfo()
}
5、值语义和引用语义
type Person struct {
name string
sex byte
}
// 指针作为接收者,引用语义
func (p *Person) SetInfoPointer() {
// 给成员赋值
(*p).name = "yoyo"
p.sex = 'f'
}
// 值作为接受者,值语义(一份拷贝)
func (p Person) SetInfoValue() {
// 给成员赋值
p.name = "yoyo"
p.sex = 'f'
}
6、指针变量的方法集
类型的方法集是值可以被该类型的值调用的所有方法的集合
结构体变量是一个指针变量,它能够调用哪些方法,这些方法就是一个集合,简称方法集
用实例 value 和 pointer 调用方法(含匿名字段)不受方法集约束,编译器总是查找全部方法,并自定传唤 receiver 实参
type Person struct {
name string
sex byte
}
func (p Person) SetInfoValue() {
}
func (p *Person) SetInfoValue() {
}
func main() {
// 结构体变量是一个指针变量,它能够调用哪些方法,这些方法就是一个集合,简称方法集
p := &Person{"mike, 'm', 18}
p.SetInfoPointer()
// 内部做了转换,先把指针 p 转换为 *p 再调用
p.SetInfoValue()
p1 := Person{"mike", 'm', 18}
p.SetInfoPointer() // 内部,先把 p 转换为 &p 再调用
}
7、方法的继承
如果匿名字段实现了一个方法,那么包含这个匿名字段的 struct 也能调用该方法。
type Person struct {
name string
sex byte
}
// Person 定义了方法
func (p *Person) PrintInfo() {
fmt.Printf("%s,%c\n", p.name, p.sex)
}
// Person 类型,实现用了一个方法
// 有个学生,继承了 Person 字段,成员和方法都继承了
type Student struct {
Person // 匿名字段,那么Student 包含了 Person 的所有字段
id int
addr string
}
8、方法的重写
type Person struct {
name string
sex byte
}
type Student struct {
Person // 匿名字段,那么Student 包含了 Person 的所有字段
id int
addr string
}
// Person 定义了方法
func (p *Person) PrintInfo() {
fmt.Printf("%s,%c\n", p.name, p.sex)
}
// Student 定义了方法(重写 Person 的方法)
func (s *Student) PrintInfo() {
fmt.Printf("%s,%c\n", s.name, s.sex)
}
func main() {
s := Student{Person{"mike", 'm', 18}, 666, "bj"}
// 就近原则:先找本作用域的方法,找不到再用继承的方法
s.PrintInfo() // 调用的是 Student 的方法
// 显式调用继承的方法
s.Person.PrintInfo()
}
9、方法值和方法表达式
方法值:隐藏接收者
方法表达式:显式的把接收者传递过去
func main() {
p := Person{"mike", 'm', 18}
p.PrintInfoPointer() // 0x24234234, &{"mike" 109 18}
// 保存方法入口地址
pFunc1 := p.PrintInfoPointer // 方法值,隐式传递 receiver
pFunc1() // 0x24234234, &{"mike" 109 18}
// 方法表达式
pFunc2 := (*Person).SetInfoPointer
pFunc2(&p) // 显示的把接收者传递过去
pFunc3 := (Person).SetInfoValue
pFunc3(p) // 显示的把接收者传递过去
}
10、接口
接口(inferface)是一个自定义类型,接口类型具体描述了一系列方法的集合。
接口类型是一种抽象的类型,它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,它们只会展示出它们自己的方法。因此接口类型不能将其实例化。
Go 通过接口实现了鸭子类型(duck-typing):"当看到一只鸟走起来想鸭子、游泳起来像鸭子、叫起来像鸭子,那么这只鸟就可以称为鸭子"。我们不关心对象是什么类型,到底是不是鸭子,只关心行为。
// 接口的定义
type Humaner inferface {
SayHi() // 方法只有声明,没有实现,由自定义类型实现
}
type Student struct {
name string
id int
}
// Student 实现了此方法
func (tmp *Student) sayHi() {
// do something
}
func main() {
// 定义接口类型变量
var I Humaner
s := &Student{"mike", 666}
i = s
i.sayhi()
}
注意:
- 接口命名习惯以 er 结尾
- 接口只有方法声明,没有实现,没有具体数据
- 接口可以匿名嵌入其他接口,或嵌入到结构中
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是由用户定义的了型实现,一个实现了这些方法的具体类型就是这个接口类型的是实例。
如果用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。
11、多态
只有一个函数,但是由不同表现
func WHoSayHi(i Humber) {
i.sayHi()
}
func main() {
s := &Student{"mike", 666}
WhoSayHi(s)
}
12、接口的继承
type Humaner interface { // 子集
sayhi()
}
type Person interface { // 超集
Humaner // 匿名字段,继承了 sayhi()
id int
}
13、空接口
空接口不包含任何方法,正因为如此,所有类型都实现了空接口。因此空接口可以存储任意类型的数值,它有点类似于 C 语言的 void* 类型。
var v1 interface{} = 1 // 将 int 类型赋值给 interface{}
var v2 interface{} = "abc" // 将 string 类型赋值给 interface{}
var v3 interface{} = struct{ X int }{1}
当函数可以接口任意的对象实例时,我们会将其声明为 interface{},最典型的例子是标准库 fmt 中 PrintXXX 系列的函数,例如:
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
14、类型查询
我们知道 interface 的变量里面可以存储任意类型的数值(该类型实现了 interface)。那么我们怎么反向知道这个变量实际保存了哪个类型的对象呢?有两种方法:
- comma-ok 断言
// 第一个返回值,第二个结果真假
value, ok := data.(type)
- switch 测试
value := data.(type)
type Student struct {
name string
id int
}
func main() {
i := make{[]interface{}, 3}
i[0] = 1
i[1] = "hi"
// 类型查询,类型断言
for index, data := range I {
// 第一个返回值,第二个结果真假
if value, ok := data.(int); ok == true {
// do something
}
}
// 第二种
for index, data := range I {
switch value := data.(type) {
case int:
// do something
}
}
}
异常、文本文件处理
1、Error
Go 语言引入一个关于错误处理的标准模式,即 error 接口,它是 Go 语言内建的接口类型,该接口的定义如下:
type error interface {
Error() string
}
Go 语言的标准库代码包 errors 提供了如下方法:
package errors
type errorString struct {
text string
}
func New(text string) error {
return &errorString{text}
}
func (e *errorString) Error() string {
return e.text
}
error 的使用
package main
import "fmt"
import "errors"
func main() {
err1 := fmt.Errorf("%s", "this is normal err1")
err2 := errors.New("this is normal err2")
}
error 的应用
package main
import "fmt"
import "errors"
func MyDiv(a, b int)(result int, err error) {
err = nil
if b == 0 {
err = errors.New("分母不能为0")
} else {
result = a / b
}
return
}
func main() {
result, err := MyDiv(10, 0)
if err != nil {
fmt.Println("err = ", err)
} else {
fmt.Println("result = ", result)
}
}
2、panic and recover
在通常情况下,向程序使用方报告错误状态的方式可以是返回一个额外的 error 类型值。
但是,当遇到不可回复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起 panic 异常。这时,上述错误处理方式显然就不适合了。反过来讲,在一般情况下,我们不应该调用 panic 函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,就应该调用 panic。
一般 panic 异常发生时,程序会中断执行,并立即执行在该 goroutine(可以理解成县城,在中被延迟的函数(defer 机制))。随后,程序崩溃并输出日志信息。日志信息包括 panic value 和函数调用的堆栈跟踪信息。
不是所有的 panic 异常都来自运行时,直接调用内置的 panic 函数也会引发 panic 异常;panic 函数接受任何值作为参数。
func panic(v interface{})
recover 用于恢复 panic 的错误
panic 和 recover 的使用
func test(x int) {
// 设置 recover
defer func() {
if err := recover(); err != nil {
// 打印错误
fmt.Println(recover())
}
}() // 调用此匿名函数
// 数组越界
type a [10]int
a[x] = 111 // 会有内置的 panic 处理,程序中断
}
func main() {
test(20)
}
3、字符串处理
- 子串判断:字符串 s 中是否包含 substr,返回 bool 值
func Contains(s, substr string) bool
- 字符串拼接:把 slice a 通过 sep 链接起来
s := []string]{"foo", "bar", "baz"}
fmt.Println(strings.Join(s, ", "))
// 运行结果:foo, bar, baz
- 子串位置: 在字符串 s 中查找 sep 所在的位置,返回位置值,找不到返回 -1
func Index(s, sep string) int
- 字符串重复多次
func Repeat(s string, count int) string
使用:
fmt.Println("ba" + strings.Repeat("na", 2))
// 运行结果:banana
- 字符串替换: 在 s 字符串中,把 old 字符串替换为 new 字符串, n 表示替换的次数,小于 0 表示全部替换
func Repleace(s, old, new string, n int) string
使用:
fmt.Println(strings.Replace("pink pink pink", "p", "s", 2))
// 运行结果:sink sink pink
- 字符串分割: 把 s 字符串按照 sep 分割,返回 slice
func Split(s, sep string) []string
使用:
fmt.Printf("%q\n", strings.Split("a,b,c", ","))
// 运行结果: ["a" "b" "c"]
- 头尾去除指定字符串
func Trim(s string, cutset string) string
使用:
fmt.Printf("[%q]", strings.Trim("!!!Achtung!!!", "!"))
// 运行结果:["Achtung"]
- 去除字符串中的空格符:返回的是 slice
func Fields(s string)[]string
使用:
fmt.Printf("Fields are %q", strings.Fields(" foo bar baz"))
// 运行结果:Fields are:["foo" "bar" "baz"]
- Append: 是将整数转换为字符串后,添加到现有的字节数组中
#import "strconv"
func main() {
slice := make([]byte, 0, 100)
slice = strconv.AppendQuote(slice, "asdfasdf")
}
- Format: 转换其他类型为字符串
a := str.FormatInt(1234, 10) // 十进制方式转换
- 字符串转换成其他类型
var flag bool
var err error
flag, err = strconv.ParseBool("true")
if err == nil {
fmt.Println("flag = ", flag)
} else {
fmt.Println("err = ", err)
}
- 字符串转换为整型
a, _ := strconv.Atoi("4567")
4、正则表达式
正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活。按照它的语法规则,随需构造出的匹配模式就能够从原始文本中筛选出几乎任何你想要的字符。
Go 语言通过 regexp 标准包为正则表达式提供了官方支持,如果你已经使用过其他编程语言提供的正则相关功能,那么你应该对 Go 语言版本的不会太陌生,但是它们之间也有一些小的差异,因为 Go 实现的是 RE2 标准。
其实字符串处理我们可以使用 strings 包进行搜索(Contains、Index)、替换(Replace)和解析(Split、Join)等操作,如果我们需要匹配可变的那种就没办法实现了,当然如果 strings 包能解决你的问题,那么就尽量使用它来解决。因为他们足够简单、而且性能和可读性都比正则更好。
小技巧:反引号中间是原生字符串,支持多行。
匹配:
\d: 数字:[0-9]
\D: 非数字
\s: 空白字符
\S: 非空白字符
\w: 单词字符[A-Za-z0-9_]
\W: 非单词字符
*: 匹配前一个字符 0 或无限次
+: 匹配前一个字符 1 或无限次
?: 匹配前一个字符 0 次或 1 次
{m}: 匹配前一个字符 m 次
{m,n}: 匹配前一个字符 m 至 n 次
^: 匹配字符串开头
&: 匹配字符串末尾
\A: 仅匹配字符串开头
\Z: 仅匹配字符串末尾
\b: 匹配 \w 和 \W 之间
import (
"fmt"
"regexp"
)
func main() {
buf := "abc azc a7c aac 888 a9c"
// 1. 解释规则,会解析正则表达式,如果成功就返回 *Regexp类型,失败会有 error 返回
reg1 regexp.MustCompile('a.c') // 匹配 a 开头,c 结尾的字符串
if reg1 == nil {
fmt.Println("err")
return
}
// 2. 根据规则提取关键信息
result := reg1.FindAllStringSubmatch(buf)
}
5、JSON
JSON(JavaScript Object Notation)是一种比 XML 更轻量级的数据交换格式,在易于人们阅读和编写的同时,也易于程序解析和生成。尽管 JSON 是 JavaSript 的一个子集,但 JSON 采用完全独立于编程语言的文本格式,且表现为键值对集合的文本描述形式(类似一些编程语言的字典结构),这使的它称为较为理想的、跨平台、跨语言的数据交换语言。
JSON 被广泛应用于 Web 服务器和客户端之间的数据通信。
Go 语言内建对 JSON 的支持。使用 Go 语言内置的 encoding/json 标准库,开发者可以轻松使用 Go 程序生成和解析 JSON 格式的数据。
{
"Company" : "it",
"Subjects" : [
"Go",
"C++"
],
"IsOk" : true
"Price" : 666
}
使用 json.Marshal() 函数可以对一组数据进行 JSON 格式化编码
func Marshal(v interface{})([]byte, error)
格式化输出:
// MarshalIndent 很像 Marshal,只是用缩进对输出进行格式化
func MarshalIndent(v interface{}, perfix, indent string)([]byte, error)
使用结构体编码成 JSON
package main
import {
"fmt"
"encoding/json"
}
type IT struct {
Company string // 后面添加 `json`:"company" 可以二次编码修改变量名称,如果是 `json`:"-" 则此字段不会输出到屏幕,如果是 `json`:",string" 则编码为字符串输出
Subjects []string
IsOk bool
}
func main() {
s := IT{"it", []string{"Go", "C++"}, true}
// 编码 -> 生成 JSON 数据
buf, err = json.Marshal(s)
if err != nil {
// 错误
return
}
fmt.Println("buf = ", string(buf))
// 格式化编码(有换行,看着更清晰)
buf, err := json.MarshalIndent(s, "", " ")
}
使用 map 编码生成 JSON
package main
import (
"encoding/json"
"fmt"
)
func main() {
// 创建一个 map
m := make(map[string]interface{}, 4)
m["company"] = "it"
m["subject"] = []string{"Go", "C++"}
m["isOk"] = false
// 编码成 json
result, err := json.Marshal(m)
if err != nil {
// 打印错误
return
}
fmt.Println("result = ", string(result))
}
JSON 的解码为结构体
jsonBuf := `{"company" : "tt"}`
var tmp IT // 定义一个结构体变量
err := json.Unmarshal([]byte(jsonBuf), &tmp) // 第二个参数要地址传递
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Println("tmp = ", tmp)
JSON 解析为 map
m := make(map[string]interface{}, 4)
err := json.Unmarshal([]byte(jsonBuf), &m) // 第二个参数要地址传递
if err != nil {
fmt.Println("err = ", err)
return
}
fmt.Println("m = ", m)
// 类型断言,值,它是 value 类型
for key, value := range m {
switch data := value.(type) {
case string:
// do something
case bool:
// do something
}
}
6、文件操作
文件分类:
- 设备文件:
- 屏幕:标准输出设备,fmt.Println()
- 键盘:标准输入设备,fmt.Scan()
- 磁盘文件:放在存储设备上的文件
- 文本文件
- 二进制文件
需要导入 os 包
新建文件
// 根据文件名创建(默认权限 0666)
func Create(name string)(file *File, err Error)
// 根据描述符创建
func NewFile(fd uintptr, name string) *File
打开文件
// 只读方式打开文件(内部实现是调用了 OpenFile)
func Open(name string)(file *File, err Error)
// flag 是打开的方式,只读,只写等,perm 是权限
func OpenFile(name string, flag int, perm uint32)
写文件
// 写入 byte 类型信息到文件(可以处理二进制文件)
func (file *File) Write(b []byte)(n int, err Error)
// 在指定位置开始写入 byte 类型信息(可以处理二进制文件)
func (file *File) WriteAt(b []byte, off int64)(n int, err Error)
// 写入 string 信息到文件(只能处理文本文件)
func (file *File) WriteString(s string)(ret int, err Error)
读文件
// 读取数据到 b 中
func (file *File) Read(b []byte)(n int, err Error)
// 从 off 开始读取数据到 b 中
func (file *File) ReadAt(b []byte, off int64)(n int, err Error)
删除文件
// 删除文件名为 name 的文件
func Remove(name string) Error
例子:
imprt "os"
func WriteFile(path string) {
// 打开文件,新建文件
f err := os.Create(path)
if err != nil {
fmt.Println("err = ", err)
return
}
// 使用完毕,需要关闭文件
defer f.Close()
var buf string
for i := 0; i < 10; i++ {
buf = fmt.Sprintf("i = %d\n", i)
fmt.Println("buf = ", buf)
n, err := f.WriteString(buf)
if err != nil {
fmt.Println("err = ", err)
}
fmt.Println("n = ", n)
}
}
func ReadFile(path string) {
// 打开文件
f, err := os.Open(path)
if err != nil {
fmt.Println("err = ", err)
return
}
// 关闭文件
defer f.Close()
buf := make([]byte, 1024 * 2) // 2k 大小
// n 代表从文件读取内容的长度
n, err1 := f.Read(buf)
if err1 != nil && err1 != io.EOF { // 文件出错,同时没有到结尾
fmt.Println("err1 = ", err1)
return
}
fmt.Println("buf = ", buf)
}
// 每次读取一行
func ReadFileLine(path string) {
// 打开文件
f, err := os.Open(path)
if err != nil {
fmt.Println("err = ", err)
return
}
// 关闭文件
defer f.Close()
// 新建一个缓冲区,把内容先放在缓冲区
r := bufio.NewReader(f)
for {
// 遇到 '\n' 结束读取,但是也会把 \n 也会读进去
buf, err := r.ReadBytes('\n')
if err != nil {
if err == io.EOF { // 文件已经结束
break
}
fmt.Println("err = ", err)
}
fmt.Printf("buf = #%s#\n", string(buf))
}
}
// 通过命令行拷贝文件
func main() {
list := os.Args // 获取命令行参数
if len(list) != 3 {
fmt:Println("usage: xxx srcFile dstFile")
return
}
srcFile = list[1]
destFile = list[2]
if srcFile == destFile {
// 错误提示
return
}
// 只读方式打开源文件
sF, err1 = os.Open(srcFileName)
if err1 != nil {
fmt.Println("err1 = ", err1)
return
}
// 新建目的文件
dF, err2 : os.Create(destFileName)
if err2 != nil {
// 错误提示
return
}
// 操作完毕,需要关闭文件
defer sF.Close()
defer dF.Close()
// 核心处理,从源文件读取内容,往目的文件写入
buf := make([]byte, 4 * 1024)//4k 大小临时缓冲区
for {
n, err := sF.Read(buf)
if err != nil {
if err == io.EOF { // 读取结束
break
}
}
dF.Write(buf[:n])
}
}