从事 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
1
suyuanhxx OP 不知道为什么表格显示错误...蛋疼
|
2
Durandcol 2018-04-24 14:55:02 +08:00
执行力好高啊.. 同去年炒币
今年 1 月份看了看 go tour 就没再学习了... |
4
kindjeff 2018-04-24 17:32:27 +08:00 1
看完了,不错的科普
|
6
mifly 2018-04-24 19:28:35 +08:00 via Android
写的不错,同写过过 Java,go,c,还是喜欢 go 多点
|
8
ebony0319 2018-04-24 19:55:39 +08:00 via Android
收藏,地铁上看。
|
9
rails3 2018-04-24 20:11:32 +08:00
int8 写成了 int6
|
11
mingyun 2018-04-24 23:38:32 +08:00
有其他语言学 go 的指南吗?比如 PHP
|
12
jiangnanyanyu 2018-04-24 23:49:19 +08:00 via Android
mark 一下
|
13
fanjianhang 2018-04-25 00:03:34 +08:00 via Android
mark
|
14
wwuha 2018-04-25 09:15:28 +08:00
mark
|
15
kangkang 2018-04-25 12:06:26 +08:00
期待楼主写一个 GO 程序学习 java 指南
|
17
jrient 2018-04-25 14:16:29 +08:00
mark
|
18
haidaochuan14 2018-04-26 14:38:46 +08:00
mark 一下
|
19
abmin521 2018-04-26 21:12:35 +08:00
interface ?
|
21
muzi 2018-04-28 18:23:27 +08:00
mark 一下
|