hello云胜

技术与生活

0%

Go VS Java

杀手级应用

java : 企业开发领域。spring

go:云原生领域。Kubernetes

相同点

都是静态语言。但是go却有类似动态语言的灵活性,比如自动的类型推断

go语言很适合重构,因为有自动的类型推断,类型重命名的功能

Go作为C语言家族的一员,和C,C++一样有节省内存、程序启动快和代码执行速度快的特点。

(但是相比其他C家族语言,go还有编译快,语法灵活,内置并发支持的优势)

跨平台性,java拥有标榜的跨平台性,通过jvm屏蔽了底层运行平台的差异。

go支持跨平台编译,比如可以在linux平台上编译出windows平台的程序,反之亦然。

关键字

GO

go的25个关键字:

break default func interface select

case defer go map struct

chan else goto package switch

const fallthrough if range type

continue for import return var

java有53个关键字,go在标榜自己语法简单时,也经常拿这个说事。

但是,仔细看下会发现,java所说的53个关键字包括了11种基本类型和2个保留字。而go的25个关键字却是剔除了基本类型。所有具体来对比应该是40Vs25。

占位符

基本类型

专门写

数组

go中的数组分为数组(array)类型和切片(slice)类型。它们最重要的不同是:数组类型的的长度是固定的,而切片类型的长度是可变长的。

数组的长度在声明它的时候就必须给定,并且在之后不会再改变。可以说,数组的长度是其类型 的一部分。

其实可以把切片看做是对数组的一层简单的封装,因为在每个切片的底层数据结构中,一定 会包含一个数组。

Go 语言的切片类型属于引用类型,而 Go 语言的数组类型则属于值类型

Go 语言里不存在像 Java 等编程语言中那种令人困惑的“传值或传引用”问题。在 Go 语言中,我们判断所谓的“传值”或者“传引用”只要看被传递的值的类型就好了。如果传递的值是引用类型的,那么就是“传引用”。如果传递的值是值类型的,那么就是“传 值”。从传递成本的角度讲,引用类型的值往往要比值类型的值低很多。

切片的扩容?

一旦一个切片无法容纳更多的元素,Go 语言就会想办法扩容。但它并不会改变原来的切片,而 是会生成一个容量更大的切片,然后将把原有的元素和新元素一并拷贝到新切片中。 在一般的情况下,你可以简单地认为新切片的容量(以下简称新容量)将会是原切片容量(以下 简称原容量)的 2 倍。 但是,当原切片的长度(以下简称原长度)大于或等于1024时,Go 语言将会以原容量的1.25 倍作为新容量的基准(以下新容量基准)。 新容量基准会被调整(不断地与1.25相乘),直到结果不小于原长度与要追加的元素数量之和 (以下简称新长度)。最终,新容量往往会比新长度大一些,当然,相等也是可能的。 另外,如果我们一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就 会以新长度为基准。 注意,与前面那种情况一样,最终的新容量在很多时候都要比新容量基准更大一些。更多细节可 参见runtime包中 slice.go 文件里的growslice及相关函数的具体实现。

List

Go 语言的链表实现在其标准库的 container/list代码包

go的list没有java的List的get(int i)方法,不能直接获取第i个元素。着实不好用。

1
2
3
4
5
6
7
8
9
l := list.New()
l.PushBack("aa")
l.PushBack(100)

for i := l.Front(); i != nil; i = i.Next() {
t.Log(i.Value)
// 类型进行了包装
t.Log(reflect.TypeOf(i).Kind())
}

container/ring包中的Ring类型实现的是一个循环链表,循环链表一旦被创建,其长度是不可 变的。

Map

go 映射过程的第一步就是把键值转换为哈希值。在 Go 语言的字典中,每一个键 值都是由它的哈希值代表的。也就是说,字典不会独立存储任何键的值,但会独立存储它们的哈 希值。

Go 语言规范规定,在键类型的值之间必须可以施加操作符==和!=。换句话说,键类型的值必 须要支持判等操作。由于函数类型、字典类型和切片类型的值并不支持判等操作,所以字典的键 类型不能是这些类型。

别名类型与潜在类型

go中的概念,java中没有对应概念。

go中有一种别名类型

1
type MyString = string

这条声明语句表示,MyString是string类型的别名类型,别名类型与其源类型的 区别恐怕只是在名称上,它们是完全相同的。

别名类型主要是为了代码重构而存在的。

Go 语言内建的基本类型中就存在两个别名类型。byte是uint8的别名类型,而rune是int32 的别名类型。

一定要注意,如果我这样声明:

1
type MyString2 string // 注意,这里没有等号。

MyString2和string就是两个不同的类型了。这里的MyString2是一个新的类型,不同于其 他任何类型。

这种方式也可以被叫做对类型的再定义。这是把string类型再定义成了另外一个类型 MyString2。

对于这里的类型再定义来说,string可以被称为MyString2的潜在类型。

潜在类型的含义是 某个类型在本质上是哪个类型或者是哪个类型的集合。

潜在类型相同的不同类型的值之间是可以进行类型转换的。

因此,MyString2类型的值与 string类型的值可以使用类型转换表达式进行互转。

但对于集合类的类型[]MyString2与[]string来说这样做却是不合法的,因为 []MyString2与[]string的潜在类型不同,分别是MyString2和string。

另外,即使两个类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也 不能赋值。

作用域

go

在同一个代码块中,可以进行变量的重声明

1
2
3
var oldParam string
// 短变量声明对已有变量进行重声明,但是要求必须有个新变量
newParam, oldParam := "newParam", "oldParam"

变量的重声明要求变量的类型必须时一样的。

注意和不同代码块中的重名变量区分。

类型判断

go

类型断言表达式的语法形式是x.(T)。其中的x代表要被判断类型的那个值。这 个值当下的类型必须是接口类型

1
value, ok := interface{}(x).([]string)

interface{}(x)将x转为接口类型,在 Go 语言中,interface{}代表空接口,任何类型都是它的实现类型

{}一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含 任何内容的数据结构(或者说数据类型)。

类型转换

T(x)

import

java

go

go中import的包,在使用其方法时,需要带上限定名

1
import "fmt"

使用时需要

1
fmt.Println("hello")

但是,如果我们把导入语句写成

1
import . "fmt"

使用的时候直接

1
Println("hello")

在这个特殊情况下,引入的包中的代码被当前源码文件中的代码,视为当前 代码包中的程序实体。go在查找当前源码文件后会先去查 用这种方式导入的那些代码包。不建议使用.引入

实现与继承

方法或函数

java中称为方法,go中称为函数

在 Go 语言中,函数可是一等的(first-class)公民,函数类型也是一等的数据类型。

简单来说,这意味着函数不但可以用于封装代码、分割功能、解耦逻辑,还可以化身为普通的 值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。

而更深层次的含义就是:函数值可以由此成为能够被随意传播的独立逻辑组件

“函数是一等的公民”是函数式编程(functional programming)的重要特征。

函数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

// 定义一个函数类型
type Printer func(content string) (n int, err error)

// 需要方法参数列表和返回值对应上,名字无所谓
func MyPrinter(name string) (num int, err error) {
return fmt.Println("myPrinter:" + name)
}

func main() {
// 声明的是函数类型
var f Printer
// 实际的函数
f = MyPrinter
// 很像java的抽象方法及其实现
f("test")
}

高阶函数

  1. 接受其他的函数作为参数传入; 或者 2. 把其他的函数作为结果返回。

闭包

方法

go中方法和函数是不同的概念,函数则是独立的程序实体。我们可以声明有名字的函数,也可以声明没名字的函数,还可以把它 们当做普通的值传来传去。我们能把具有相同签名的函数抽象成独立的函数类型,以作为一组输 入、输出(或者说一类逻辑组件)的代表。

方法却不同,它需要有名字,不能被当作值来看待,最重要的是,它必须隶属于某一个类型。

方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指 针类型。

Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之 间的组合。

通道

通道(channel)作为 Go 语言最有特色的数据类型,完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。

Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)

这是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言,这也充分体现了 Go 语言最重要 的编程理念。

通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类 型。

  1. 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。

  2. 发送操作和接收操作中对元素值的处理都是不可分割的。

  3. 发送操作在完全完成之前会被阻塞。接收操作也是如此。

1
2
3
4
5
6
7
8
func Test_channel(t *testing.T) {
ch1 := make(chan string, 3)
ch1 <- "a"
ch1 <- "b"
ch1 <- "c"
recv := <-ch1
t.Log("收到第一个元素:", recv)
}

make(chan string, 3) string是该通道的类型,也就是了我们可以通 过这个通道传递什么类型的数据。

后面的3表示通道的容量。所谓通道的容量,就是指通道最多可以缓存多少个元素 值。

当容量为0时,或者不设置容量时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,我 们可以称为缓冲通道,也就是带有缓冲的通道。

一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按 照发送的顺序排列的,先被发送通道的元素值一定会先被接收。

在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会 执行对同一个通道的任意个发送或接收操作中的某一个。

对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。

这里要注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是 在接收操作符右边的那个元素值,而是它的副本!!

另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成 正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。

这两步操作是一个原子操作。

阻塞问题

先说针对缓冲通道的情况。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有 元素值被接收走。这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发 送操作。相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。 这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。 因此而等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等

对于非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直 到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。 也就是说,只有收发双方对接上了,数据才会被传递。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲 通道则在用异步的方式传递数据。

单向通道

单向通道最主要的用途就是约束其他代码的行为。

select语句联用

select语句只能与通道联用。由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达 式,比如接收表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func Test_channel4(t *testing.T) {
intChannels := [3]chan int{
make(chan int, 1),
make(chan int, 1),
make(chan int, 1),
}

// 随机选择一个发送

index := rand.Intn(3)
intChannels[index] <- index
t.Logf("向channe%d发送", index)

select {
case e := <-intChannels[0]:
{
t.Logf("0接收到:%d", e)
}
case e := <-intChannels[1]:
t.Logf("1接收到:%d", e)
case e := <-intChannels[2]:
t.Logf("2接收到:%d", e)
default:
t.Log("都没选中")
}

像这样加入了default分支,那么无论涉及通道操作的表达式是否有阻塞, select语句都不会被阻塞。default会被选中执行。

如果没有加入默认分支,那么一旦所有的case表达式都没有满足求值条件,那么select 语句就会被阻塞。直到至少有一个case表达式满足条件为止。

select语句只能对其中的每一个case表达式各求值一次。所以,如果我们想连续或定时 地操作其中的通道的话,就往往需要通过在for语句中嵌入select语句的方式实现。但这 时要注意,简单地在select语句的分支中使用break语句,只能结束当前的select语句而并不会对外层的for语句产生作用。

仅当select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。

如果select语句发现同时有多个候选分支满足选择条件,那么它就会用一种伪随机的算法 在这些分支中选择一个并执行。

并发

Go 语言不但有着独特的并发编程模型,以及用户级线程 goroutine,还拥有强大 的用于调度 goroutine、对接系统级线程的调度器。

这个调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的 三个主要元素,即:G(goroutine 的缩写)、P(processor 的缩写)和 M(machine 的缩 写)。 其中的 M 指代的就是系统级线程。而 P 指的是一种可以承载若干个 G,且能够使这些 G 适时 地与 M 进行对接,并得到真正运行的中介

从宏观上说,G 和 M 由于 P 的存在可以呈现出多对多的关系。当一个正在与某个 M 对接并运 行着的 G,需要因某个事件(比如等待 I/O 或锁的解除)而暂停运行的时候,调度器总会及时 地发现,并把这个 G 与那个 M 分离开,以释放计算资源供那些等待运行的 G 使用。 而当一个 G 需要恢复运行的时候,调度器又会尽快地为它寻找空闲的计算资源(包括 M)并安 排运行。另外,当 M 不够用时,调度器会帮我们向操作系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

正因为调度器帮助我们做了很多事,所以我们的 Go 程序才总是能高效地利用操作系统和计算机 资源。

主 goroutine

与一个进程总会有一个主线程类似,每一个独立的 Go 程序在运行时也总会有一个主 goroutine。这个主 goroutine 会在 Go 程序的运行准备工作完成后被自动地启用,并不需要我 们做任何手动的操作。

主 goroutine 的go函数就是那个作为程序入口的main函数

当程序执行 到一条go语句的时候,Go 语言的运行时系统,会先试图从某个存放空闲的 G 的队列中获取一 个 G(也就是 goroutine),它只有在找不到空闲 G 的情况下才会去创建一个新的 G。

然而,创建 G 的成本也是非常低的。创建一个 G 并不会像新建一个进程或者一个系统级线程那 样,必须通过操作系统的系统调用来完成,在 Go 语言的运行时系统内部就可以完全做到了

在拿到了一个空闲的 G 之后,Go 语言运行时系统会用这个 G 去包装当前的那个go函数(或者 说该函数中的那些代码),然后再把这个 G 追加到某个存放可运行的 G 的队列中。

请记住,只要go语句本身执行完毕,Go 程 序完全不会等待go函数的执行,它会立刻去执行后边的语句。这就是所谓的异步并发地执行。

异常处理

error

1
2
3
type error interface {
Error() string
}

error类型其实是一个接口类型,也是一个 Go 语言的内建类型。在这个接口类型的 声明中只包含了一个方法Error。这个方法不接受任何参数,但是会返回一个string类型的结 果。

panic

Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需 任何参数,并且会返回一个空接口类型的值。

defer

注意, 被延迟执行的是defer函数,而不是defer语句。

defer语句和recover函数调用,才能够恢复一个已经发生的 panic。

如果一个函数中有多条defer语句,那么那几个defer函数调用的执行顺序是怎样 的?

如果只用一句话回答的话,那就是:在同一个函数中,defer函数调用的执行顺序与它们分别 所属的defer语句的出现顺序(更严谨地说,是执行顺序)完全相反。 当一个函数即将结束执行时,其中的写在最下边的defer函数调用会最先执行,其次是写在它 上边、与它的距离最近的那个defer函数调用,以此类推,最上边的defer函数调用会最后一 个执行。

垃圾回收