万字总结!go语言速成教程(适用于有基础同学)


前言

这篇博客完全是因为对go突然产生了兴趣才开始的,无奈实习岗位太少,暑期还是准备去做java全栈了,虽然都说java卷(更吃学历了),但是go生态真的暂时落后太多,所以还是老老实实去做java了,go之后有缘再做吧,(大厂的话内部转go,尤其是java转go,是挺常见的,尤其是字节)。

也是考虑了很多,最后还是选了java。实习打开boss、实习僧,清一色的java,go寥寥无几,大部分都是北上广深。问了一些学长学姐,大家本科出来也都是走的后端开发岗,身边也有部分走数据分析的(运维、测试就算了)。走嵌入式方向,硬件也没基础,还是老老实实开发岗比较合适。c++游戏开发,也是很可以的,还有一些时间,如果下学期找不到实习的话也去做一两个游戏项目吧,也不能太all in java吧。

这篇博客前面部分还是自己学过来的,后面到了高级应用是一塌糊涂,拿了很多人家的博客内容,勉强拼凑完了,唉,要去做了java,不然开学实习就不好投了,希望之后还有精力分享java学习和实习面试内容。

go

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 前的几个常见争议问题:

  1. Go 语言长久以来的依赖管理问题。
  2. “淘汰”现有的 GOPATH 的使用模式。
  3. 统一社区中的其它的依赖管理工具(提供迁移功能)。

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有缘再修改这篇博客吧。

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值