文章目录
前言
这篇博客完全是因为对go突然产生了兴趣才开始的,无奈实习岗位太少,暑期还是准备去做java全栈了,虽然都说java卷(更吃学历了),但是go生态真的暂时落后太多,所以还是老老实实去做java了,go之后有缘再做吧,(大厂的话内部转go,尤其是java转go,是挺常见的,尤其是字节)。
也是考虑了很多,最后还是选了java。实习打开boss、实习僧,清一色的java,go寥寥无几,大部分都是北上广深。问了一些学长学姐,大家本科出来也都是走的后端开发岗,身边也有部分走数据分析的(运维、测试就算了)。走嵌入式方向,硬件也没基础,还是老老实实开发岗比较合适。c++游戏开发,也是很可以的,还有一些时间,如果下学期找不到实习的话也去做一两个游戏项目吧,也不能太all in java吧。
这篇博客前面部分还是自己学过来的,后面到了高级应用是一塌糊涂,拿了很多人家的博客内容,勉强拼凑完了,唉,要去做了java,不然开学实习就不好投了,希望之后还有精力分享java学习和实习面试内容。
1. go语言优势
- 可直接编译成机器码,不依赖其他库。
- 静态类型语言是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,写起来的效率很高。
- 语言层面支持并发,这个就是Go最大的特色,天生的支持并发。Go就是基因里面支持的并发,可以充分的利用多核,很容易的使用并发。
- 内置runtime,支持垃圾回收,这属于动态语言的特性之一吧,虽然目前来说GC(内存垃圾回收机制)不算完美,但是足以应付我们所能遇到的大多数情况,特别是Go1.1之后的GC。
- 简单易学,Go语言的作者都有C的基因,那么Go自然而然就有了C的基因,那么Go关键字是25个,但是表达能力很强大,几乎支持大多数你在其他语言见过的特性:继承、重载、对象等。
- 丰富的标准库,Go目前已经内置了大量的库,特别是网络库非常强大。
- 内置强大的工具,Go语言里面内置了很多工具链,最好的应该是gofmt工具,自动化格式化代码,能够让团队review变得如此的简单,代码格式一模一样,想不一样都很困难。
- 跨平台编译,如果你写的Go代码不包含cgo,那么就可以做到window系统编译linux的应用,如何做到的呢?Go引用了plan9的代码,这就是不依赖系统的信息。
- 内嵌C支持,Go里面也可以直接包含C代码,利用现有的丰富的C库。
2. go语言不足
- 泛型缺乏: 尽管在Go 1.18版本中引入了泛型,但其功能相对其他语言仍然有限,可能不够成熟,使用起来也不如C++或Java中的泛型灵活。
- 错误处理: Go采用显式的错误返回机制,而不是异常处理。这种方法虽然简单明确,但在处理复杂的错误逻辑时,代码显得繁琐且冗长,容易导致大量的重复代码。
- 依赖管理: 尽管Go模块系统(Go Modules)已经有所改进,但在处理依赖管理方面,尤其是版本控制和依赖冲突上,仍然存在一些问题。
- 不支持动态加载: Go不支持像Java那样的动态类加载,这在某些需要动态扩展或插件化的应用场景中会显得不够灵活。
- 生态系统较小: 相对于Java、Python等流行语言,Go的生态系统和第三方库相对较少,特别是在一些特定领域,这点相比java来说是很大的劣势。
- GUI开发支持有限: Go在桌面应用程序的开发支持上较为有限,虽然有一些第三方库,如fyne和Qt bindings,但整体上不如Java或C#等语言的GUI开发工具成熟。
3. go适合用来做什么
- 服务器编程,以前你如果使用C或者C++做的那些事情,用Go来做很合适,例如处理日志、数据打包、虚拟机处理、文件系统等。
- 网络编程,这一块目前应用最广,包括Web应用、API应用、下载应用。
- 内存数据库,如google开发的groupcache,couchbase的部分组建。
- 云平台,目前国外很多云平台采用Go开发,CloudFoundy的部分组建,前VMare的技术总监自己出来搞的apcera云平台。
- 分布式系统,数据库代理器等。
4. 环境搭建与编译器
这个可以看其他文章(点击访问网址),go的环境变量配置中,goroot是go的安装目录,相当于java中的jdk,gopath是go的项目工作目录,存放项目代码,个人比较推荐用vscode来编写go语言程序。
第一章 基础语法
1. Hello word!
go代码需要声明package包名,通过import引入我们的库(第三方库或者自定义库)。其中package main表示一个可独立执行的程序,每个 go 应用程序都包含一个名为 main 的包(并不是包名叫做main)。
func main()是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
注意:这里面go语言的语法,定义函数的时候,‘{’ 必须和函数名在同一行,不能另起一行。
package main
import (
"fmt"
)
func main() {//不能另起一行
fmt.Println("Hello word!")
}
2. 变量声明
go支持匿名变量,用_
(空白标识符)来代替变量,比如:_, str := test()
,test()返回两个值。
- 指定变量类型
var a int=0
(没有赋值则为默认值) - 不指定变量类型
var a=0
(自动识别) - 省略var关键字
a :=0
(这种不可以用于声明全局变量)
多变量声明:
var ( //这种分解的写法,一般用于声明全局变量
a int
b float32
)
var aa,bb = 123, "go"
3. 常量(const和iota)
const在c++经常使用,在go语言中同样表示常量,即不可修改的变量。
- 显式类型定义:
const b string = "abc"
(是否声明变量类型) - 隐式类型定义:
const b = "abc"
常量可以计算表达式的值len(),cap(),unsafe.Sizeof()等,也就是说常量可以作为参数被传入到这些函数中。
const (
a = "abc" //abc
b = len(a) //3
c = unsafe.Sizeof(a) //16
)
//unsafe.Sizeof(a)输出的结果是16
//字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,
//这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。
//组成可以理解成此结构体
type string struct
{
Data uintptr // 指针占8个长度
Len int // 长度64位系统占8个长度
}
注:常量表达式中,函数必须是内置函数。
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1。有点像我们c++的迭代器,每次都会进行自增。
const (
Unknown = 0
Female = 1
Male = 2
)//普通的const枚举
//采用iota进行枚举
const (
a = iota //0
b //b=1 ,iota = 1
c //c=2, iota = 2
d = "ha" //独立值,结果为ha,iota=3
e //"ha", iota=4
f = 100 //iota =5
g //100 iota =6
h = iota //7,恢复计数
i //8
j = "nihao"//nihao iota=9
k = "shijie"//shijie iota=10
l = iota//11
m //12
)
4. 函数
4.1 函数结构
在go中,函数名称首字母大写是公有方法,首字母小写是私有方法,变量也是。
不像c++和java通过public,private等关键字来对变量和函数进行修饰以控制作用域。
基本的结构如下:
func Name(/*参数列表*/) (a type1, b type2/*返回类型*/) {
//函数体
return v1, v2 //返回多个值
}
根据参数和返回值有无,简单举下面几个例子,这个也和其他语言类似:
func Test1(v1 int, v2 int) { //有参无返回
//或者func Test02(v1, v2 int)
fmt.Printf("v1 = %d, v2 = %d\n", v1, v2)
}
//形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数
func Test2(args ...int) {//可变参数,传入若干个int型变量
for _, n := range args { //忽略索引,遍历参数列表
fmt.Println(n)
}
}
多个返回值:
func Test01() (int, string) { //方式1
return 1, "go"
}
func Test02() (a int, str string) { //方式2, 给返回值命名
a = 1
str = "go"
return
}
init()函数,构造函数,和其它语言执行顺序一样,不多赘述。
main函数,只能在package main中。
注:一个包会被多个包同时导入,那么它只会被导入一次,也就是只执行一次init()。
4.2 函数传参
两种传参方式,值传递和引用传递,和c++的传参机制类似,go的传参还是很简单的。
- 值传递是指在调用函数时将实际参数复制一份传递到函数中,不影响实际参数。
- 引用传递(指针传递)
函数中引用传递用*
变量指向地址用&
4.3 匿名函数和闭包
这个很重要!
匿名函数是指没有名字的函数,可以在声明的同时定义和调用。匿名函数非常适合用于需要一次性使用的函数,或者将函数作为参数传递给其他函数的场景。
package main
import "fmt"
func main() {
// 定义并调用一个匿名函数
func(message string) {
fmt.Println(message)
}("Hello, World!")//()的作用是,此处直接调用此匿名函数
}
闭包是指函数可以捕获并记住其作用域外的变量。闭包使得这些变量的值在函数调用后仍然保持不变,或者在不同的函数调用间共享这些变量的状态。
或者理解为定义在一个函数内部的函数,是将函数内部和函数外部连接起来的媒介。
package main
import "fmt"
func main() {
// 创建一个闭包
counter := func() func() int {//函数名为func(),返回值为func() int,传参为null
count := 0
return func() int {
count++
return count
}
}()//()的作用是,此处直接调用此匿名函数
// 调用闭包
fmt.Println(counter()) // 输出: 1
fmt.Println(counter()) // 输出: 2
fmt.Println(counter()) // 输出: 3
}
count变量的生命周期却不会因为函数执行完而结束。这是因为count变量在内函数外部定义,被这个匿名函数所引用,所以它会在整个程序的运行期间保留下来。
5. 类型转换
go语言中不允许隐式转换,所有类型转换必须显式声明,而且转换只能发生在两种相互兼容的类型之间。
var ch byte = 97
//var a int = ch //err, cannot use ch (type byte) as type int in assignment
var a int = int(ch)
类型别名
type bigint int64 //int64类型改名为bigint
var x bigint = 100
type (
myint int //int改名为myint
mystr string //string改名为mystr
)
6. defer关键字
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
什么意思呢?
c++的析构函数,是在函数生命周期结束后执行的函数。defer也类似,相当于延迟执行函数。
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行,包括导包的顺序,就类似于析构函数,defer的执行在return语句之后。
注意,defer语句只能出现在函数或方法的内部。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
释放资源的defer应该直接跟在请求资源的语句后。
func main() {
a, b := 10, 20
defer func(x int) { // a以值传递方式传给x
fmt.Println("defer:", x, b) // b 闭包引用
}(a)
a += 10
b += 100
fmt.Printf("a = %d, b = %d\n", a, b)
/*
运行结果:
a = 20, b = 120
defer: 10 120
*/
}
7. 数组与slice
go的数组定义var a[10] int {1, 2, 3}
,但是定长数组在函数传参时需要声明长度,a[10] int 和 a[4] int是不同的变量,而且这样只是浅拷贝(值拷贝),这些是传参时的区别。
还有数组遍历,可以用for也可以用range函数,这个前面有讲,返回索引(下标)和数组元素值,常使用匿名变量来丢掉索引值。
7.1 定义切片
slice(切片),可以认为是动态数组。slice定义比较简单,有点像c语言需要用malloc()动态分配内存。而且slice传参是引用传递,就像在c++中,数组本质上就是指针。
//1. 直接创建,只需要中括号内不声明元素个数就默认动态数组
var a []type
//2. 声明数组,使用make()函数来开辟空间,有点像c语言
var slice1 []type = make([]type, len)
//也可以简写为:
slice1 := make([]type, len)
//3.也可以指定容量,其中capacity为可选参数
make([]T, length, capacity)
切片扩容是以cap为单位进行扩容,超过cap容量时,会自动扩展成2*cap。
7.2 切片初始化
//1. 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s :=[] int {1,2,3 }
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}//也可以用...表示
balanced := [5]float32(1:2.0, 3:7.0)//声明部分元素
//2. 初始化切片s,是数组arr的引用
s:=arr[:]
//3. 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片,首位若缺省就是默认
s := arr[startIndex:endIndex]
//4. 通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
//5. 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片,cap是容量
s :=make([]int,len,cap)
7.3 切片常用操作
① len() :获取切片长度。
对切片进行截取时,可以认为是通过指针来实现的,不是浅拷贝。
② cap() :测量切片最长可以达到多少。
一个切片在未初始化之前默认为 nil(类似于null),长度为 0。
③ 切片截取:可以通过设置下限及上限来设置截取切片[lower-bound:upper-bound]。
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int,0,5)
printSlice(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
结果如下:
len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]
④ append() 添加元素:
//在切片尾部追加N个元素
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
//在切片开头位置添加元素
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头位置添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
//append链式操作
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
⑤ copy() 拷贝:
拷贝 numbers 的内容到 numbers1,返回复制的元素个数。
`copy(numbers1,numbers)`
⑥ 删除元素
删除元素本质上是通过后续元素覆盖需要删除的元素,简单来说就是通过赋值,使得后续元素向前不断覆盖。从中间删除就只需要保持前面元素不变,后面元素向前滚动即可。
从头部删除:
//append在这里主要用链式操作
//1. 直接移动数据指针:
a = []int{1, 2, 3, ...}
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原地完成:
//append删除操作如下:
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1], ...)
a = append(a[:i], a[i+N:], ...)
//copy删除操作如下:
a = []int{1, 2, 3}
a = a[:copy(a[:i], a[i+1:])] // 删除中间1个元素
a = a[:copy(a[:i], a[i+N:])] // 删除中间N个元素
从尾部删除:
a = []int{1, 2, 3, ...}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
8. map
map是go中的数据结构,底层原理是哈希表,是一个无序的key—value对的集合。(这个在其他语言中也经常使用,java和c++都有map类和set,python也有字典和元组)
map的格式:
map[keyType]valueType
。
注:map是无序的。
//map定义
package main
import (
"fmt"
)
func main() {
//第一种声明
var test1 map[string]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
test1 = make(map[string]string, 10)
test1["one"] = "php"
test1["two"] = "golang"
test1["three"] = "java"
fmt.Println(test1) //map[two:golang three:java one:php]
//第二种声明
test2 := make(map[string]string)
test2["one"] = "php"
test2["two"] = "golang"
test2["three"] = "java"
fmt.Println(test2) //map[one:php two:golang three:java]
//第三种声明
test3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java",
}
}
//map的遍历
m1 := map[int]string{1: "mike", 2: "yoyo"}
//迭代遍历1,第一个返回值是key,第二个返回值是value
for k, v := range m1 {
fmt.Printf("%d ----> %s\n", k, v)
//1 ----> mike
//2 ----> yoyo
}
//增删改查
val, key := language["php"] //查找是否有php这个子元素
if key {
fmt.Printf("%v", val)
} else {
fmt.Printf("no");
}
language["php"]["id"] = "3" //修改了php子元素的id值
language["php"]["nickname"] = "yes" //增加php元素里的nickname值
delete(language, "php") //删除了php子元素
9. struct结构体
go的结构体和c的很像,结构体定义,结构体指针以及结构体的值、引用传递,更多的是在面向对象的使用,这里就不多说了,直接看下面例子。
type Student struct {
id int
name string
sex byte
age int
addr string
}
func main() {
var s5 *Student = &Student{3, "xiaoming", 'm', 16, "bj"}
s6 := &Student{4, "rocco", 'm', 3, "sh"}
}
//两个结构体将可以使用 == 或 != 运算符进行比较
10. go面向对象
go的面向对象都是通过struct来实现的,也就是说没有严格的类class,都是通过函数和结构体来实现类似于面向对象特点的。
- 封装:通过方法实现
- 继承:通过匿名字段实现
- 多态:通过接口实现
10.1 封装(方法实现)
在封装中,类的方法是通过函数实现的,当函数传入参数为该结构体,并且首字母为大写,说明只有该结构体对应这个函数,就类似于类的方法,go中没有this关键字。
package main
import "fmt"
//定义一个结构体
type T struct {
name string
}
func (t T) method1() {
t.name = "new name1"
}
func (t *T) method2() {
t.name = "new name2"
}
func main() {
t := T{"old name"}
fmt.Println("method1 调用前 ", t.name)
t.method1()
fmt.Println("method1 调用后 ", t.name)
fmt.Println("method2 调用前 ", t.name)
t.method2()
fmt.Println("method2 调用后 ", t.name)
}
运行结果:
method1 调用前 old name
method1 调用后 old name
method2 调用前 old name
method2 调用后 new name2
10.2 继承(struct实现)
简单来说继承就是结构体中结构体。
//人
type Person struct {
name string
sex byte
age int
}
//学生
type Student struct {
Person // 匿名字段,那么默认Student就包含了Person的所有字段
id int
addr string
}
func main() {
//顺序初始化
s1 := Student{Person{"mike", 'm', 18}, 1, "sz"}
//s1 = {Person:{name:mike sex:109 age:18} id:1 addr:sz}
fmt.Printf("s1 = %+v\n", s1)
//s2 := Student{"mike", 'm', 18, 1, "sz"} //err
//部分成员初始化1
s3 := Student{Person: Person{"lily", 'f', 19}, id: 2}
//s3 = {Person:{name:lily sex:102 age:19} id:2 addr:}
fmt.Printf("s3 = %+v\n", s3)
//部分成员初始化2
s4 := Student{Person: Person{name: "tom"}, id: 3}
//s4 = {Person:{name:tom sex:0 age:0} id:3 addr:}
fmt.Printf("s4 = %+v\n", s4)
}
对成员操作
var s1 Student //变量声明
//给成员赋值
s1.name = "mike" //等价于 s1.Person.name = "mike"
s1.sex = 'm'
s1.age = 18
s1.id = 1
s1.addr = "sz"
fmt.Println(s1) //{{mike 109 18} 1 sz}
var s2 Student //变量声明
s2.Person = Person{"lily", 'f', 19}
s2.id = 2
s2.addr = "bj"
fmt.Println(s2) //{{lily 102 19} 2 bj}
同名字段,这点显示go在继承中存在覆盖。
//人
type Person struct {
name string
sex byte
age int
}
//学生
type Student struct {
Person // 匿名字段,那么默认Student就包含了Person的所有字段
id int
addr string
name string //和Person中的name同名
}
func main() {
var s Student //变量声明
//给Student的name,还是给Person赋值?
s.name = "mike"
//{Person:{name: sex:0 age:0} id:0 addr: name:mike}
fmt.Printf("%+v\n", s)
//默认只会给最外层的成员赋值
//给匿名同名成员赋值,需要显示调用
s.Person.name = "yoyo"
//Person:{name:yoyo sex:0 age:0} id:0 addr: name:mike}
fmt.Printf("%+v\n", s)
}
10.3 多态(interface实现)
10.3.1 interface
多态是通过接口来实现的,java中通过override和overload来实现,而go的实现如下:
- l 接⼝命名习惯以 er 结尾。
- l 接口只有方法声明,没有实现,没有数据字段。
- l 接口可以匿名嵌入其它接口,或嵌入到结构中。
在下面的代码中,Humaner就相当于是c++的一个父类,我们的子类都要实现所有父类的方法(要传入指针才可以进行修改)。我们对父类实例化,可以指向不同子类,此时同一接口就实现了多态。
type Humaner interface {
SayHi()
}
type Student struct { //学生
name string
score float64
}
//Student实现SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s, %f] say hi!!\n", s.name, s.score)
}
type Teacher struct { //老师
name string
group string
}
//Teacher实现SayHi()方法
func (t *Teacher) SayHi() {
fmt.Printf("Teacher[%s, %s] say hi!!\n", t.name, t.group)
}
type MyStr string
//MyStr实现SayHi()方法
func (str MyStr) SayHi() {
fmt.Printf("MyStr[%s] say hi!!\n", str)
}
//普通函数,参数为Humaner类型的变量i
func WhoSayHi(i Humaner) {
i.SayHi()
}
func main() {
s := &Student{"mike", 88.88}
t := &Teacher{"yoyo", "Go语言"}
var tmp MyStr = "测试"
s.SayHi() //Student[mike, 88.880000] say hi!!
t.SayHi() //Teacher[yoyo, Go语言] say hi!!
tmp.SayHi() //MyStr[测试] say hi!!
//多态,调用同一接口,不同表现
WhoSayHi(s) //Student[mike, 88.880000] say hi!!
WhoSayHi(t) //Teacher[yoyo, Go语言] say hi!!
WhoSayHi(tmp) //MyStr[测试] say hi!!
x := make([]Humaner, 3)
//这三个都是不同类型的元素,但是他们实现了interface同一个接口
x[0], x[1], x[2] = s, t, tmp
for _, value := range x {
value.SayHi()
}
}
当接口中还存在其他接口时的调用:
type Humaner interface {
SayHi()
}
type Personer interface {
Humaner //这里想写了SayHi()一样
Sing(lyrics string)
}
type Student struct { //学生
name string
score float64
}
//Student实现SayHi()方法
func (s *Student) SayHi() {
fmt.Printf("Student[%s, %f] say hi!!\n", s.name, s.score)
}
//Student实现Sing()方法
func (s *Student) Sing(lyrics string) {
fmt.Printf("Student sing[%s]!!\n", lyrics)
}
func main() {
s := &Student{"mike", 88.88}
var i2 Personer
i2 = s
i2.SayHi() //Student[mike, 88.880000] say hi!!
i2.Sing("学生哥") //Student sing[学生哥]!!
}
接口可以转换。
10.3.2 空接口interface{}和类型断言
golang中的所有程序都实现了interface{}的接口,也就是说所有变量都可以使用 interface{} 来进行代替(类似于java中的Object类),当我们不清楚传入什么类型参数时,可以用空接口代替。
package main
import "fmt"
func funcName(a interface{}) string {
value, ok := a.(string)//判断
if !ok {
fmt.Println("It is not ok for type string")
return ""
}
fmt.Println("The value is ", value)
return value
}
func main() {
var a int = 10
funcName(a)
}
类型断言就是用于判断变量类型,常用两种断言方法:
① comma-ok断言
- value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
- 如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
- 代码略
② switch测试
var t interface{}
t = functionOfSomeType()
switch t := t.(type) {
default:
fmt.Printf("unexpected type %T", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
11. 反射
value是实际变量值,type是实际变量的类型。一个interface{}类型的变量包含了2个指针,一个指针指向值的类型【对应concrete type】,另外一个指针指向实际的值【对应value】。
type: static || concrete 要么是静态类型,要么是具体类型:
pair = type + value
变量结构:
package main
import "fmt"
type Reader interface {
ReadBook()
}
type Writer interface {
WriterBook()
}
// 具体类型-结构体
type BookOne struct {
}
// 重写ReadBook
func (this *BookOne) ReadBook() {
fmt.Println("Read a Book")
}
// 重写WriterBook
func (this *BookOne) WriterBook() {
fmt.Println("Writer a Book")
}
func main() {
// ================================================================================
//var a string
pair<static type, value:"zhangsan">
//a = "zhangsan"
//var allType interface{}
//
//allType = a
//str,_ := allType.(string)
//fmt.Println(allType)
//fmt.Println(str)
// ================================================================================
1.打开文件流
tty: pair<type:*os.File, value:"/Users/a1234/Desktop/test/test.docx"文件描述符>
//tty, err := os.OpenFile("/Users/a1234/Desktop/test/test.txt", os.O_RDWR, 0)
//if err != nil {
// fmt.Println("open file error", err)
// return
//}
//
2.读取文件
r: pair<type: , value: >
//var r io.Reader
r: pair<type:*os.File, value:"/Users/a1234/Desktop/test/test.docx"文件描述符>
//r = tty
//
3.写入文件
w: pair<type: , value: >
//var w io.Writer
w: pair<type:*os.File, value:"/Users/a1234/Desktop/test/test.docx"文件描述符>
//w = r.(io.Writer)
//
//w.Write([]byte("w: pair<type:*os.File, value:\"/Users/a1234/Desktop/test/test.docx\"文件描述符>"))
// ================================================================================
// b: pair<type:BookOne, value:BookOne{}地址>
b := &BookOne{}
// b: pair<type: , value: >
var r Reader
// r: pair<type:BookOne, value:BookOne{}地址>
r = b
r.ReadBook()
var w Writer
// r: pair<type:BookOne, value:BookOne{}地址>
w = r.(Writer)// r.(Writer)断言为什么会成功,因为w r具体的type是一致的
w.WriterBook()
}
Golang的指定类型的变量的类型是静态的(也就是指定int、string这些的变量,它的type是static type),在创建变量的时候就已经确定,反射主要与Golang的interface类型相关(它的type是concrete type),只有interface类型才有反射一说。
interface及其pair的存在,是Golang中实现反射的前提,理解了pair,就更容易理解反射。反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。
package main
import (
"fmt"
"io"
"os"
)
func main() {
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
fmt.Println("open file error", err)
return
}
var r io.Reader
r = tty
var w io.Writer
w = r.(io.Writer)
w.Write([]byte("HELLO THIS IS A TEST!!!\n"))
}
TypeOf和ValueOf
既然反射就是用来检测存储在接口变量内部(值value;类型concrete type) pair对的一种机制。那么在Golang的reflect反射包中有什么样的方式可以让我们直接获取到变量内部的信息呢? 它提供了两种类型(或者说两个方法)让我们可以很容易的访问接口变量内容,分别是reflect.ValueOf() 和 reflect.TypeOf()。
// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero
func ValueOf(i interface{}) Value {...}
//ValueOf用来获取输入参数接口中的数据的值,如果接口为空则返回0
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}
//TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil
从relfect.Value中获取接口interface的信息
当执行reflect.ValueOf(interface)之后,就得到了一个类型为”relfect.Value”变量,可以通过它本身的Interface()方法获得接口变量的真实内容,然后可以通过类型判断进行转换,转换为原有真实类型。
12. 结构体标签
package main
import (
"fmt"
"reflect"
)
type resume struct {
Name string `json:"name" doc:"我的名字"`
}
func findDoc(stru interface{}) map[string]string {
t := reflect.TypeOf(stru).Elem()
doc := make(map[string]string)
for i := 0; i < t.NumField(); i++ {
doc[t.Field(i).Tag.Get("json")] = t.Field(i).Tag.Get("doc")
}
return doc
}
func main() {
var stru resume
doc := findDoc(&stru)
fmt.Printf("name字段为:%s\n", doc["name"])
}
第二章 go高级应用
1. 异常处理
我们都知道go的异常处理非常繁琐,下面我们来看一下go的语法。
1.1 error接口
Go语言引入了一个关于错误处理的标准模式,即error接口,它是Go语言内建的接口类型,该接口的定义如下:
type error interface {
Error() string
}
示例代码:
import (
"errors"
"fmt"
)
func main() {
var err1 error = errors.New("a normal err1")
fmt.Println(err1) //a normal err1
var err2 error = fmt.Errorf("%s", "a normal err2")
fmt.Println(err2) //a normal err2
}
import (
"errors"
"fmt"
)
func Divide(a, b float64) (result float64, err error) {
if b == 0 {
result = 0.0
err = errors.New("runtime error: divide by zero")
return
}
result = a / b
err = nil
return
}
func main() {
r, err := Divide(10.0, 0)
if err != nil {
fmt.Println(err) //错误处理 runtime error: divide by zero
} else {
fmt.Println(r) // 使用返回值
}
}
1.2 panic
在通常情况下,向程序使用方报告错误状态的方式可以是返回一个额外的error类型值。
但是,当遇到不可恢复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起painc异常。这时,上述错误处理方式显然就不适合了。反过来讲,在一般情况下,我们不应通过调用panic函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用panic。
一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。
不是所有的panic异常都来自运行时,直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。
func TestA() {
fmt.Println("func TestA()")
}
func TestB() {
panic("func TestB(): panic")
}
func TestC() {
fmt.Println("func TestC()")
}
func main() {
TestA()
TestB()//TestB()发生异常,中断程序
TestC()
}
1.3 recover
运行时panic异常一旦被引发就会导致程序崩溃。这当然不是我们愿意看到的,因为谁也不能保证程序不会发生任何运行时错误。
不过,Go语言为我们提供了专用于“拦截”运行时panic的内建函数——recover。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
func recover() interface{}
注意:recover只有在defer调用的函数中有效。
如果调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。
func TestA() {
fmt.Println("func TestA()")
}
func TestB() (err error) {
defer func() { //在发生异常时,设置恢复
if x := recover(); x != nil {
//panic value被附加到错误信息中;
//并用err变量接收错误信息,返回给调用者。
err = fmt.Errorf("internal error: %v", x)
}
}()
panic("func TestB(): panic")
}
func TestC() {
fmt.Println("func TestC()")
}
func main() {
TestA()
err := TestB()
fmt.Println(err)
TestC()
/*
运行结果:
func TestA()
internal error: func TestB(): panic
func TestC()
*/
}
2. goroutine
协程:coroutine。也叫轻量级线程。
goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
Go语言中的并发程序主要使用两种手段来实现:goroutine和channel。
//goroutine示例
package main
import (
"fmt"
"time"
)
func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(1*time.Second) //延时1s
}
}
func main() {
//创建一个 goroutine,启动另外一个任务
go newTask()
i := 0
//main goroutine 循环打印
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(1 * time.Second) //延时1s
}
}
Goexit函数
调用 runtime.Goexit() 将立即终止当前 goroutine 执⾏,调度器确保所有已注册 defer 延迟调用被执行。
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
runtime.Goexit() // 终止当前 goroutine, import "runtime"
fmt.Println("B") // 不会执行
}()
fmt.Println("A") // 不会执行
}() //不要忘记()
//死循环,目的不让主goroutine结束
for {
}
}
3. channel
channel是Go语言中的一个核心类型,可以把它看成管道。并发核心单元通过它就可以发送或者接收数据进行通讯,这在一定程度上又进一步降低了编程的难度。
channel是一个数据类型,主要用来解决go程的同步问题以及go程之间数据共享(数据传递)的问题。
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
引⽤类型 channel可用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
定义一个channel时,也需要定义发送到channel的值的类型。channel可以使用内置的make()函数来创建:
chan是创建channel所需使用的关键字。Type 代表指定channel收发数据的类型。
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者和被调用者将引用同一个channel对象。和其它的引用类型一样,channel的零值也是nil。
当 参数capacity= 0 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。
channel非常像生活中的管道,一边可以存放东西,另一边可以取出东西。channel通过操作符 <- 来接收和发送数据,发送和接收数据语法:
channel <- value //发送value到channel
<-channel //接收并将其丢弃
x := <-channel //从channel中接收数据,并赋值给x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空
3.1 无缓冲的channel
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何数据值的通道。
这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。否则,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。
阻塞:由于某种原因数据没有到达,当前go程(线程)持续处于等待状态,直到条件满足,才解除阻塞。
同步:在两个或多个go程(线程)间,保持数据内容一致性的机制。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 0) //创建无缓冲的通道 c
//内置函数 len 返回未被读取的缓冲元素数量,cap 返回缓冲区大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子go程正在运行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延时2s
for i := 0; i < 3; i++ {
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main进程结束")
}
3.2 有缓冲的channel
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个数据值的通道。
这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也不同。
只有通道中没有要接收的值时,接收动作才会阻塞。
只有通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
func main() {
c := make(chan int, 3) //带缓冲的通道
//内置函数 len 返回未被读取的缓冲元素数量, cap 返回缓冲区大小
fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))
go func() {
defer fmt.Println("子go程结束")
for i := 0; i < 3; i++ {
c <- i
fmt.Printf("子go程正在运行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
}
}()
time.Sleep(2 * time.Second) //延时2s
for i := 0; i < 3; i++ {
num := <-c //从c中接收数据,并赋值给num
fmt.Println("num = ", num)
}
fmt.Println("main进程结束")
}
3.3 关闭channel
如果发送者知道,没有更多的值需要发送到channel的话,那么让接收者也能及时知道没有多余的值可接收将是有用的,因为接收者可以停止不必要的接收等待。这可以通过内置的close函数来关闭channel实现。
package main
import (
"fmt"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
for {
//ok为true说明channel没有关闭,为false说明管道已经关闭
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("Finished")
}
4. select
Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
有时候我们希望能够借助channel发送或接收数据,并避免因为发送或者接收导致的阻塞,尤其是当channel没有准备好写或者读时。select语句就可以实现这样的功能。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。
与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语句。
如果其中的任意一语句可以继续执行(即没有被阻塞),那么就从那些可以执行的语句中任意选择一条来使用。
如果没有任意一条语句可以执行(即所有的通道都被阻塞),那么有两种可能的情况:
l 如果给出了default语句,那么就会执行default语句,同时程序的执行会从select语句后的语句中恢复。
l 如果没有default语句,那么select语句将被阻塞,直到至少有一个通信可以进行下去。
package main
import (
"fmt"
)
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
5. Go Modules
Go modules 是 Go 语言的依赖解决方案,发布于 Go1.11,成长于 Go1.12,丰富于 Go1.13,正式于 Go1.14 推荐在生产上使用。
Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了,而 Go modules 的出现也解决了在 Go1.11 前的几个常见争议问题:
- Go 语言长久以来的依赖管理问题。
- “淘汰”现有的 GOPATH 的使用模式。
- 统一社区中的其它的依赖管理工具(提供迁移功能)。
Go Modoules的目的之一就是淘汰GOPATH(之前有)。
go mod命令
命令 | 作用 |
---|---|
go mod init | 生成 go.mod 文件 |
go mod download | 下载 go.mod 文件中指明的所有依赖 |
go mod tidy | 整理现有的依赖 |
go mod graph | 查看现有的依赖结构 |
go mod edit | 编辑 go.mod 文件 |
go mod vendor | 导出项目所有的依赖到vendor目录 |
go mod verify | 校验一个模块是否被篡改过 |
go mod why | 查看为什么需要依赖某模块 |
后记
就先到这里结束吧,之后如果和go有缘再修改这篇博客吧。