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 | l := list.New() |
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 | var oldParam string |
变量的重声明要求变量的类型必须时一样的。
注意和不同代码块中的重名变量区分。
类型判断
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 | package main |
高阶函数
- 接受其他的函数作为参数传入; 或者 2. 把其他的函数作为结果返回。
闭包
方法
go中方法和函数是不同的概念,函数则是独立的程序实体。我们可以声明有名字的函数,也可以声明没名字的函数,还可以把它 们当做普通的值传来传去。我们能把具有相同签名的函数抽象成独立的函数类型,以作为一组输 入、输出(或者说一类逻辑组件)的代表。
方法却不同,它需要有名字,不能被当作值来看待,最重要的是,它必须隶属于某一个类型。
方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指 针类型。
Go 语言中根本没有继承的概念,它所做的是通过嵌入字段的方式实现了类型之 间的组合。
通道
通道(channel)作为 Go 语言最有特色的数据类型,完全可以与 goroutine(也可称为 go 程)并驾齐驱,共同代表 Go 语言独有的并发编程模式和编程哲学。
Don’t communicate by sharing memory; share memory by communicating. (不要通过共享内存来通信,而应该通过通信来共享内存。)
这是作为 Go 语言的主要创造者之一的 Rob Pike 的至理名言,这也充分体现了 Go 语言最重要 的编程理念。
通道类型的值本身就是并发安全的,这也是 Go 语言自带的、唯一一个可以满足并发安全性的类 型。
对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
发送操作和接收操作中对元素值的处理都是不可分割的。
发送操作在完全完成之前会被阻塞。接收操作也是如此。
1 | func Test_channel(t *testing.T) { |
make(chan string, 3) string是该通道的类型,也就是了我们可以通 过这个通道传递什么类型的数据。
后面的3表示通道的容量。所谓通道的容量,就是指通道最多可以缓存多少个元素 值。
当容量为0时,或者不设置容量时,我们可以称通道为非缓冲通道,也就是不带缓冲的通道。而当容量大于0时,我 们可以称为缓冲通道,也就是带有缓冲的通道。
一个通道相当于一个先进先出(FIFO)的队列。也就是说,通道中的各个元素值都是严格地按 照发送的顺序排列的,先被发送通道的元素值一定会先被接收。
在同一时刻,Go 语言的运行时系统(以下简称运行时系统)只会 执行对同一个通道的任意个发送或接收操作中的某一个。
对于通道中的同一个元素值来说,发送操作和接收操作之间也是互斥的。
这里要注意的一个细节是,元素值从外界进入通道时会被复制。更具体地说,进入通道的并不是 在接收操作符右边的那个元素值,而是它的副本!!
另一方面,元素值从通道进入外界时会被移动。这个移动操作实际上包含了两步,第一步是生成 正在通道中的这个元素值的副本,并准备给到接收方,第二步是删除在通道中的这个元素值。
这两步操作是一个原子操作。
阻塞问题
先说针对缓冲通道的情况。如果通道已满,那么对它的所有发送操作都会被阻塞,直到通道中有 元素值被接收走。这时,通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发 送操作。相对的,如果通道已空,那么对它的所有接收操作都会被阻塞,直到通道中有新的元素值出现。 这时,通道会通知最早等待的那个接收操作所在的 goroutine,并使它再次执行接收操作。 因此而等待的、所有接收操作所在的 goroutine,都会按照先后顺序被放入通道内部的接收等
对于非缓冲通道,情况要简单一些。无论是发送操作还是接收操作,一开始执行就会被阻塞,直 到配对的操作也开始执行,才会继续传递。由此可见,非缓冲通道是在用同步的方式传递数据。 也就是说,只有收发双方对接上了,数据才会被传递。并且,数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。相比之下,缓冲 通道则在用异步的方式传递数据。
单向通道
单向通道最主要的用途就是约束其他代码的行为。
select语句联用
select语句只能与通道联用。由于select语句是专为通道而设计的,所以每个case表达式中都只能包含操作通道的表达 式,比如接收表达式。
1 | func Test_channel4(t *testing.T) { |
像这样加入了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 | type error interface { |
error类型其实是一个接口类型,也是一个 Go 语言的内建类型。在这个接口类型的 声明中只包含了一个方法Error。这个方法不接受任何参数,但是会返回一个string类型的结 果。
panic
Go 语言的内建函数recover专用于恢复 panic,或者说平息运行时恐慌。recover函数无需 任何参数,并且会返回一个空接口类型的值。
defer
注意, 被延迟执行的是defer函数,而不是defer语句。
defer语句和recover函数调用,才能够恢复一个已经发生的 panic。
如果一个函数中有多条defer语句,那么那几个defer函数调用的执行顺序是怎样 的?
如果只用一句话回答的话,那就是:在同一个函数中,defer函数调用的执行顺序与它们分别 所属的defer语句的出现顺序(更严谨地说,是执行顺序)完全相反。 当一个函数即将结束执行时,其中的写在最下边的defer函数调用会最先执行,其次是写在它 上边、与它的距离最近的那个defer函数调用,以此类推,最上边的defer函数调用会最后一 个执行。