从事 java 编码也有三年多了,写的Java
代码也很多。Go
语言,我是无意间接触到的。在去年 12 月份左右的时候比特币大涨到 1w 刀,就想着研究下比特币,而同时有听说 Go 语言在区块链中非常火爆。就抱着学着看看的心情了解了Go
,不知不觉喜欢上Go
语言的简洁和优雅了。
Go
语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。
Go
语言的高并发支持,语法的简洁性,指针的自动垃圾回收,可以让开发人员将精力放在业务处理上。Go
语言能够将开发人员带入更生层次的去了解底层操作系统,而不仅局限于语言本身。Go
语言的"Less is more",需要在编码过程中去体会这种设计哲学!
以数据交换为例子,Go 语言实现:
array[i] , array[j] = array[j] , array[i] // Go
用Java
实现:
int temp = array[i];
array[i] = array[j];
array[j] = temp;
在语法上Go
相对Java
有着很多开发人员非常喜欢的特性。可以说这些特性,设计的非常人性化。大大简化了开发人员的工作。语法上从以下四点介绍Java
和Go
区别
变量,赋值操作
在变量命名规则上Java
和Go
基本相同,都是大小写字母数字,下划线,不能以数字开头。但在Go
中以大写字母开头的变量、方法、函数、结构体都表示public
(对所有类可见)类型,小写表示private
(本类可见)类型(这里不讨论下划线开头)。而Java
中用public
private
关键字明确表示,没有明确表示的为default
(包可见)类型,有比较严格的访问级别。Go
语言特意淡化了此点,可以节省很多代码。但也为初学者留下了一些小坑,比如JSON
序列化时,属性字段必须大写才能序列化。
例如:(Go
语言以换行符表示一行代码结束,Java
以;
表示结束,此法对Go
依然适用)
type Response struct {
Message string `json:"message"`//将 Message 字段序列化成 JSON 中的 message
userName string `json:"userName"`//首字母小写,无法被序列化
}
在变量声明赋值上,Go
语言采用现代化“脚本语言”声明方式。Go
支持类型自动推断,故在声明变量时可以将变量类型省略。以下一组代码说明变量a
b
c
d
的多种声明赋值方式,当多个变量赋值时推荐使用最后一种方式,代码量最少。相对于Java
一行代码只能声明一个变量,要便捷很多。
函数外部申明必须使用var
,不要采用:=
。
var a int
a = 10
var b int = 10
var c = 10
d := 10
var a, b, c, d = 10
a, b, c, d := 10
a, b, c, d := 10, 10, 11, 12
数据类型
Go
语言中的数据类型与Java
差距比较大,Go
常用基本数据类型继承了C/C++
语言特点。
Java
中有包装类型,包装类型为对象,Go
没有此功能。Go
中还包含一些其他的数据类型简写: byte
=> uint8
;int
=> uint
;uint
=> uint32
或uint64
;rune
=> int32
;uintptr
表示无符号整型,用于存放一个指针。
下面表格具体表示了Go
和Java
数据类型对比:
Go
的数据类型比Java
更加丰富,但使用起来上两者相差不多。Go
中没有包装类型,大大减少Java
中思考使用包装类型还是简单类型的时间。Go
中也引入了无符号,有符号数据类型的选择。
在处理中文字符上,Go
和Java
一样方便。
指针,slice
,map
Go
语言仍采用了C/C++
中的指针,相对于Java
稍显复杂。但由于Go
中能够自动垃圾回收,只需要学会灵活使用指针就能够大幅减少内存操作时间,简化代码。可以说 Go 语言指针是C/C++
和Java
的中合。能够享受指针的便捷,同时又省去手动释放指针的麻烦。
指针变量可以指向任何一个值所在的内存地址。Go
中使用*
来声明一个指针,同时也是取指针值操作符。&
用来取内存地址,16 进制,例如0xc420014608
,不同的机器,不同的环境内存地址都会不同。
var p *int//声明一个指针
a:=10
p=&a //指针赋值
fmt.Println(p) //打印指针所指向内存地址 0xc420014608
fmt.Println(&a) //a 与 p 所指向内存中同一地址 0xc420014608
fmt.Println(&p) //取变量 p 地址,会变, 0xc42000e048
fmt.Println(*p) //打印指针内容, 10
slice
slice
和Java
中的List
很像,都是能够自动扩容的数组集合。Go
中List
实现了栈和队列的功能,和Java List
差距比较大。在结构上Go slice
为动态素组,可以自动根据当前元素和声明的容量进行扩容。使用起来非常方便,但可能会带来性能的消耗,频繁的扩容将使代码运行速率下降。
slice
有两个额外的属性:len
(长度),capacity
(容量), var c = make([]int, capacity)
slice
为数组引用,改变slice
的值会导致原数组也会发生变化。这种现象在Go
中非常常见,使用时要格外注意。
var a [4]int // 数组
a = [4]int{1, 2, 3, 4}// 数组赋值
b := a[1:3] //slice,数组 a 的引用,从 a 数组下标 1 到下标 2
b[1] = 5 //a={1,2,5,4}, b={2,5}
而在Java
中其实也是存在这种现象的,不过 Java 中“一切皆是对象”的原则,我们在创建一个新的变量或者对象时,都是使用 new 操作,然后再赋值,所以这种对象引用的该变导致原对象也被改变的情况比较少。变量作为参数时,这种情况发生较多,以下 Java 代码也时有发生:
List<Integer> a = new ArrayList<>();
a.add(1);
a.add(2);
List<Integer> b = a;
b.add(5);
slice
有两个非常重要的函数copy()
、append()
。append
函数用于给slice
追加元素,当 slice 容量不够时会自动扩容,例如b = append(b, 3)
;copy
函数用于将一个数组拷贝到另一个数组,不会自动扩容,例如:
copy(b, []int{7,3,8}) // b={7,3},由于 b 的长度只有 2,copy 只会复制前两位
map
map
和Java
中的HashMap
就设计思想有很多相似的地方,都是非线程安全的数据结构。Java
中HashMap
底层采用数组(bucket
)+链表+红黑树(红黑树为JDK1.8
新增部分)的数据结构。都是考虑到Map
的使用场景,牺牲线程安全提升访问效率的实现方式。并发情况下使用ConcurrentHashMap
。
Go map
使用方式相比Java
更为简单,任何数据类型都可以作为key
(Java
中key
必须为对象,基本数据类型的封装类型才能作为 key ),例如:
var m map[int]string // 声明
m[1]="1" // 赋值
fmt.Println(m[1]) // 取值
如何用 Go 实现一个线程安全的 map ?最直接的方式就是在 map 读写的时候加上读写锁...
type ConcurrentHashMap struct {
lock *sync.RWMutex //Read and write Lock
cm map[interface{}]interface{}
}
func (m *ConcurrentHashMap) Get(k interface{}) interface{} {
m.lock.RLock() //Read Lock
defer m.lock.RUnlock()
if val, ok := m.cm[k]; ok {
return val
}
return nil
}
func (m *ConcurrentHashMap) Set(k interface{}, v interface{}) bool {
m.lock.Lock() // Write Lock
defer m.lock.Unlock()
if val, ok := m.cm[k]; !ok {
m.cm[k] = v
} else if val != v {
m.cm[k] = v
} else {
return false
}
return true
}
Go map
是hash
结构的,意味着平均访问时间是 O(1)的。同传统的hashmap
一样,由一个个bucket
组成,bucket
内部又由一个指针数组组成。按key
的类型采用相应的 hash 算法得到key
的hash
值。将hash
值的低位当作 Hmap 结构体中 buckets 数组的 index,找到 key 所在的 bucket。将 hash 的高 8 位存储在了bucket
的tophash
中。注意,这里高 8 位不是用来当作key/valu
e 在bucket
内部的offset
的,而是作为一个主键,在查找时对tophash
数组的每一项进行顺序匹配的。先比较hash
值高位与bucket
的tophash[i]
是否相等,如果相等则再比较bucket
的第i
个的key
与所给的key
是否相等。如果相等,则返回其对应的value
,反之,在overflow buckets
中按照上述方法继续寻找。slice
,map
的遍历都可直接采用range
关键字,foreach
的形式遍历,也可采用for
循环方式遍历。foreach
遍历和Java
相同,都不宜在遍历过程中改变原值。方法函数
对于习惯了 Java 的开发人员来说,总是会将方法和函数当成一个概念,但在 Go 中这两个确有一些不同之处。但形式差不多,功能相同。
func
定义,完成特定功能,可以有多个返回值。例如:func Add(a int, b int) int { ... }
。形式上和 Java 有一定差距,功能作用和 Java 一样。不过 Go 中能够有多个返回值,这极大的简化了实际功能的完成。一下几个例子说明方法的声明:func Add(a int, b int) int { ... }
func Sub(a int, b int) (result int) { ... }// result 为返回对象,可在方法体中对 result 直接赋值
func Multi(a int, b int) (int, string) { ... } //多个返回值必须是用括号
func Divide(a int, b int) (result int, s tring) { ... }//如果多个返回值中其中一个有返回值变量,其他的也要有返回值变量
func (user *User) getUserName() string { ... }
示例中的User
为一个结构体,getUserName
为结构体的一个方法。面向对象
Go 中面向对象要简单很多,去除了 Java,C/C++中复杂的继承关系,保留了接口interface
和struct
。interface
中包含的是方法,struct
中只能定义属性。strcut
能够实现interface
中的方法(函数)。两者相结合使用实现面向对象的思想,相比 Java 和 C/C++简洁了不少,概念也减少了很多。面向对象的思想还在,仍存在对象关联(父子类)关系。
type People interface {
getUserName() string
}
type User struct {
Name string
}
func (user *User) getUserName() string {
return user.Name
}
func main() {
var p People
p = &User{Name: "zhangshan"} //使用 User 初始化 p,p 为接口 People 的实现,只包含方法 getUserName()
fmt.Print(p.getUserName()) // print "zhangshan"
}
在Go
代码中我们要防止接口的滥用,相比Java
中让人头疼的继承和接口实现,Go
要相对简单,但我们在使用interface
时需要考虑是否必要。
Go
语言中的错误处理相比Java
中的try catch
要相对简单一点,但从另一方面 Go 中的Errors are values
却又麻烦很多!习惯了Java
中的try
抛出异常,catch
中处理异常的开发人员来说,可能对于Go
中的error
和panic
处理方式会有点不能适应。
在Java
中我们通常处理异常(错误)的方式为:将一段可能出错的业务代码使用try catch
包裹。try
中进行正常业务逻辑,在catch
模块对业务逻辑进行补偿操作,事务回滚等,在finally
模块对资源进行释放。看起来是一段相对严谨的处理逻辑,大多数开发人员处理到这就结束了。但这其中却包含很多不确定性,请看如下代码:
try{
A.close();
}catch(Exception e){
try{
B.close();
}catch(Exception e){
B.close();
}finally{
B.close();
}
}finally{
try{
C.close();
}catch(Exception e){
C.close();
}finally{
C.close();
}
}
对于不确定的值,开发人员都会尝试去捕捉,并做出处理,但上述冗长的代码,感觉非常糟糕,最终的情况可能是 ABC 三个链接都没法正常关闭。Java
中try catch
给我们代码方便的同时,却留下很大的“操作空间”,这可能就陷入了一个死胡同。偷懒的开发人员可能会直接放弃处理,而且大多数开发人员都会如此。因为这实在是太冗长了。在实际开发过程中,catch 中往往只做了打印异常的功能,很多开发人员补偿都不会做!
而Go
就将这种情况摆在开发人员的面前(Java
中开发人员能够睁一只眼闭一只眼),你无法忽视这个问题。Go
的error
设计是,错误也是一种合法的值——“ Errors are values ”
err := Sub()
if err != nil { //与常规处理思路相反,优先处理错误
fmt.Print(err)
return err
}
...//正常业务逻辑
Go
中的异常panic
会导致整个Go
程序 crash (进一步说明Go
中的异常必须处理,不可忽略),防止异常导致整个程序崩溃,我们要使用recover()
来进行恢复。
同时引入关键字defer
来延迟执行 defer 后面的函数,非常适合用来处理异常和错误。多条 defer 函数的处理顺序和声明顺序相反。Go 中有很多正确处理错误的实践方式,需要在实际编码过程中体会。
Go 的并发模型设计来自 CSP 模型。Golang 借用 CSP 模型的一些概念为之实现并发进行理论支持,其实从实际上出发,go 语言并没有完全实现了 CSP 模型的所有理论。仅仅是借用了 process 和 channel 这两个概念。相对于 Java 的并发设计,使用起来要方便很多,因此 Go 可以轻易的起成千上万个协程( Goroutine )。(这里并不会讲述 Go 调度器模型)
Goroutine 是实际并发执行的实体,它底层是使用协程(coroutine)实现并发。coroutine 是一种运行在用户态的用户线程,类似于 greenthread,go 底层选择使用 coroutine 的出发点是因为,它具有以下特点:
goroutine 是在 golang 层面提供了调度器,并且对网络 IO 库进行了封装,屏蔽了复杂的细节,对外提供统一的语法关键字支持,简化了并发程序编写的成本。Go 中并发编程示例:
channel
控制并发,channel
又分为带缓冲buffer
和不带缓冲buffer
。不带缓冲的 channel 不能在同一个 gorutine 读写,否者发生死锁。带缓冲的 channel 可以。
func main() {
ch := make(chan int) // 声明一个不带缓冲的 channel,ch := make(chan int,2) 带缓冲 channel,缓冲为 2
go func() { // go 关键字表示启动一个协程 goroutine
ch <- 1 // channel <- 表示向 channel 中写数据
}()
fmt.Println(<-ch) // i:=<- channle 表示读取 channel 中的数据到变量 i
}
sync.WaitGroup
控制并发
fun main(){
var wg sync.WaitGroup
var urls = []string{"http://www.golang.org/", "http://www.google.com/"}
for _, url := range urls {
wg.Add(1) //每有一个 goroutinie,wg+1
go func(url string) {
defer wg.Done() // 函数最后将该协程 goroutine 标记为完成
http.Get(url)
}(url)
}
wg.Wait() // 等待所有的 goroutine 完成后再执行下面的代码
}
context
实现并发控制。context
主要是用来处理goroutine
中又开启其他gourine
,达到跟踪goroutine
的解决方案。主要是用来处理多个goroutine
之间共享数据,及多个goroutine
的管理。Go
语言有指针,也有自动垃圾回收。这一点上与Java
一致,都采用了标记清除算法,不过Go
中还有另外两种垃圾回收算法:位图标记和内存布局,精确的垃圾回收。
讨论垃圾回收,就需要知道为什么要有垃圾回收,那就需要先了解系统是如何分配内存。操作系统中有一个内存池。首先,它会向操作系统申请大块内存,自己管理这部分内存。然后,它是一个池子,当上层释放内存时它不实际归还给操作系统,而是放回池子重复利用。这样反复的过程中,内存管理中必然会出现内存碎片问题,当代码中需要申请一个较大的对象时,原用的碎片空间已经不够使用,这就出现了垃圾回收。
垃圾回收有着非常长的历史,第一批垃圾回收算法是为单核机器和小内存程序而设计的。那个时候,CPU 和内存价格昂贵,而且用户没有太多的要求,即使有明显的停顿也没有关系。这个时期的算法设计更注重最小化回收器对 CPU 和堆内存的开销。也就是说,除非内存不足,否则 GC 什么事也不做。而当内存不足时,程序会被暂停,堆空间会被标记并清除,部分内存会被尽快释放出来。
分代理论假说,大部分的内存对象“朝生夕死”,它们在分配到内存不久之后就被作为垃圾回收。这就是分代理论假说的基础,它是整个软件产品线 领域最贴合实际的发现。数十年来,在软件行业,这个现象在各种编程语言上表现出惊人的一致性,不管是函数式编程语言、命令式编程语言、没有值类型的编程语言,还是有值类型的编程语言。现代垃圾回收器基本上都是基于分代算法。分代回收器可以加入其它各种特性,一个现代回收器将会集并发、并行、压缩和分代于一身。
例如 Java 中 JVM 的 GC 分为“年轻代”和“老年代”,“年轻代”的对象大多“朝生夕死”,能够存活下来的对象会放到老年代中。是典型的分代回收器。
下面简单分析集中垃圾回收算法:
标记清除算法
我们都知道标记清除算法会“ stop the world ”。内存单元并不会在变成垃圾立刻回收,而是保持不可达状态,直到到达某个阈值或者固定时间长度。这个时候系统会挂起用户程序,也就是 STW,转而执行垃圾回收程序。
该算法中有一个标记初始的 root 区域,以及一个受控堆区。root 区域主要是程序运行到当前时刻的栈和全局数据区域。在受控堆区中,很多数据是程序以后不需要用到的,这类数据就可以被当作垃圾回收了。判断一个对象是否为垃圾,就是看从 root 区域的对象是否有直接或间接的引用到这个对象。如果没有任何对象引用到它,则说明它没有被使用,因此可以安全地当作垃圾回收掉。
标记清扫算法分为两阶段:标记阶段和清扫阶段。标记阶段( Go 采用的是三色标记法),从 root 区域出发,扫描所有 root 区域的对象直接或间接引用到的对象,将这些对上全部加上标记。在回收阶段,扫描整个堆区,对所有无标记的对象进行回收。)more
位图标记和内存布局 既然垃圾回收算法要求给对象加上垃圾回收的标记,显然是需要有标记位的。一般的做法会将对象结构体中加上一个标记域,一些优化的做法会利用对象指针的低位进行标记,这都只是些奇技淫巧罢了。Go 没有这么做,它的对象和 C 的结构体对象完全一致,使用的是非侵入式的标记位.
精确垃圾回收
通过定位对象的类型信息,得到该类型中的垃圾回收的指令码,通过一个状态机解释这段指令码来执行特定类型的垃圾回收工作。对于堆中任意地址的对象,找到它的类型信息过程为,先通过它在的内存页找到它所属的 MSpan,然后通过 MSpan 中的类型信息找到它的类型信息。more
Go
作为一个现代化后端程序语言,搭建微服务架构当然也是没有任何问题的。Go 通常采用Go-Kit
作微服务框架,Java
通常采用Spring Cloud
做微服务架构。
微服务的架构主要关键包含以下几点:
服务拆分
服务拆分粒度主要看具体业务需求,不易太宽泛,也最好不要太细,不然就会产生几十个微服务,维护成本较大。
服务治理(服务注册发现)
服务注册发现又分为两种:
Java 微服务实践中通常使用Spring Cloud
框架,使用Eureka
作为微服务的服务注册表,spring 封装了Eureka
,让 Eureka 即作服务转发又作服务注册表。
Go 中可以使用Consul
(一个用于发现和配置的服务。提供了一个 API 允许客户端注册和发现服务。Consul 可以用于健康检查来判断服务可用性),etcd
(一个高可用,分布式的,一致性的,键值表,用于共享配置和服务发现。两个著名案例包括 Kubernetes 和 Cloud Foundry)
远程调用 RPC
在RPC
方面Java
和Go
均可采用GRPC
、Apache thrift
或者直接采用Restful
风格的Http
请求。Java
也可采用dubbo
封装的RPC
方式
高可用,负载均衡
服务治理能够在服务与服务之间时间负载均衡。
HTTP
反向代理和负载据衡器(例如NGINX
)可以用于服务发现负载均衡器。服务注册表可以将路由信息推送到NGINX
,激活一个实时配置更新;例如,可以使用 Consul Template
。NGINX Plus
支持额外的动态重新配置机制,可以使用DNS
,将服务实例信息从注册表中拉下来,并且提供远程配置的API
。
网关,路由追踪
理论上说,一个客户端可以直接给多个微服务中的任何一个发起请求。每一个微服务都会有一个对外服务端(https://serviceName.api.company.name
)。这个 URL 可能会映射到微服务的负载均衡上,它再转发请求到具体节点上。
通常来说,一个更好的解决办法是采用API Gateway
的方式。API Gateway
是一个服务器,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的 Facade 模式很像。API Gateway
封装内部系统的架构,并且提供 API 给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等。
Java
中通常使用zuul
来搭建服务器网关。
最后给大家推荐我的几个 Repo
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.