[转] Java 程序员的 Golang 入门笔记
最近抽空学习了一下
Go
语言,好多特性感觉非常棒,由于高效的开发效率以及性能,现在好多优秀的开源项目都是基于Go
开发,比如Docker
、etcd
、consul
、Kubernetes
等。Go
势必会在互联网技术的服务化,容器化的将来大展拳脚。正好网上看到一篇关于Java
程序员入门Golang
的文章,写的挺好的,所以特此转载过来,再加上自己的一些学习经验,供大家参考。
Golang
从 09
年发布,中间经历了多个版本的演进,已经渐渐趋于成熟,其媲美于 C
语言的性能、Python
的开发效率,又被称为 21 世纪的 C
语言,尤其适合开发后台服务。这篇文章主要是介绍 Golang
的一些主要特性,和 Java
做一个对比,以便更好的理解 Golang
这门语言。
关于 Golang
环境的搭建就不讲了,可以参考
官方文档
或者大神
astaxie
的开源书籍
build-web-application-with-golang
的相关篇章。下面我没让你就从 Go
版本的 Hello World
开始。
Hello World
每种语言都有自己的 Hello World
,Go
也不例外,Go
版本的如下:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World!你好,世界!")
}
我们使用 go run
运行后,会在控制台终端看到 Hello World!你好,世界!
的输出。我们来看下这段代码:
package
是一个关键字,定义一个包,和Java
里的package
一样,也是模块化的关键。main
包是一个特殊的包名,它表示当前是一个可执行程序,而不是一个库。import
也是一个关键字,表示要引入的包,和Java
的import
关键字一样,引入后才可以使用它。fmt
是一个包名,这里表示要引入fmt
这个包,这样我们就可以使用它的函数了。main
函数是主函数,表示程序执行的入口,Java
也有同名函数,但是多了一个String[]
类型的参数。Println
是fmt
包里的函数,和Java
里的System.out.println
作用类似,这里输出一段文字。
整段代码非常简洁,关键字、函数、包等和 Java
非常相似,不过注意,go
是不需要以 ;
(分号)结尾的。
变量
go
语言变量的声明和 java
的略有不同,以声明一个 int
类型,变量名为 age
为例,go
语言变量生成如下:
var age int =10
同样的变量,在 java
中的声明是:
int age = 10;
可以看到 go
的变量声明,修饰变量的类型在变量的后面,而且是以 var
关键字开头。
var 变量名 类型 = 表达式
最后面的赋值可以在声明的时候忽略,这样变量就有一个默认的值,称之为 零值
。零值
是一个统称,以类型而定,比如 int
类型的零值为 0
,string
类型的零值是 ””
空字符串。
在 go
中除了以 var
声明变量之外,还有一种简短的变量声明方式 :=
,比如上面例子,可以如下简单声明:
age := 10
这种方式和上面的例子等价,但是少了 var
和变量类型,所以简短方便,用的多。使用这种方式,变量的类型由 go
根据值推导出来,比如这里默认是 int
。
不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用 var
方式来定义全局变量。
var a1, a2 string = "1", "d"
Go
对于已声明但未使用的变量(局部变量)会在编译阶段报错
常量
有了变量,就少不了常量,和 var
关键字不一样,go
的常量使用 const
声明,这个和 C
里的常量一样。
const age = 10
这样就声明了一个常量 age
,其值是 10
,因为我们这里没有指定常量的类型,所以常量的类型是根据值推导出来的。所以等价的我们也可以指定常量类型,如下:
const age int = 10
相比来说,java
下的常量定义就要复杂一些,要有 static final
修饰符,才是常量:
private static final int AGE = 10;
这个和 go
的实现等价,但是它的定义修饰符比 go
多多了,而且常量类型不能省略。
大小写标记访问权限
我们上面的 go
例子中我特意用了小些的变量名 age
,甚至常量我也没有写成 AGE
,但是在 java
中,对于常量我们的习惯是全部大些。
在 go
中不能随便使用大小写的问题,是因为大小写具有特殊意义,在 go
中,大些字母开头的变量或者函数等是 public
的,可以被其他包访问;小些的则是private
的,不能被其他包访问到。这样就省去了 public
和 private
声明的烦恼,使代码变的更简洁。
特别说明,这些导出规则只适用于包级别名字定义,不能使函数内部的定义。
包
包的规则和java
很像,每个包都有自己独立的空间,所以可以用来做模块化,封装,组织代码等。
和java
不同的是, go
的包里可以有函数,比如我们常用的fmt.Println()
,但是在在java
中没有这种用法,java
的方法必须是属于一个类或者类的实例的。
要使用一个包,就需要先导入,使用import
关键字,和java
也一样,可以参见前面的hello world
示例。
如果我们需要导入多个包的时候,可以像java
一样,一行行导入,也可以使用快捷方式一次导入,这个是java
所没有的。
import (
"io"
"log"
"net"
"strconv"
)
类型转换
go
对于变量的类型有严格的限制,不同类型之间的变量不能进行赋值、表达式等操作,必须要要转换成同一类型才可以,比如int32
和int64
两种int
类型的变量不能直接相加,要转换成一样才可以。
var a int32 = 13
var b int64 = 20
c := int64(a) + b
这种限制主要是防止我们误操作,导致一些莫名其妙的问题。在java
中因为有自动转型的概念,所以可以不同类型的可以进行操作,比如int
可以和double
相加,int
类型可以通过+
和字符串拼接起来,这些在go
中都是不可行的。
map
map
类型,Java
里是Map
接口, go
里叫做字典,因为其常用,在 go
中,被优化为一个语言上支持的结构,原生支持,就像一个关键字一样,而不是java
里的要使用内置的sdk
集合库,比如HashMap
等。
ages := make(map[string]int)
ages["linday"] = 20
ages["michael"] = 30
fmt.Print(ages["michael"])
go
里要创建一个map
对应,需要使用关键字make
,然后就可以对这个map
进行操作。
map
的结构也非常简单,符合KV模型,定义为map[key]value
, 方括号里是key
的类型,方括号外紧跟着对应的value
的类型,这些明显和Java
的Map
接口不同。如果在 go
中我们要删除map
中的一个元素怎么办?使用内置的delete
函数就可以,如下代码删除ages
这个map
中,key
为michael
的元素。
delete(ages,"michael")
如果我们想遍历map
中的K、V
值怎么办?答案是使用range
风格的for
循环,可比Java Map
的遍历简洁多了。
for name,age := range ages {
fmt.Println("name:",name,",age:",age)
}
range
一个map
,会返回两个值,第一个是key
,第二个是value
,这个也是go
多值返回的优势,下面会讲。
函数方法
在 go
中,函数和方法是不一样的,我们一般称包级别的(直接可以通过包调用的)称之为函数,比如fmt.Println();
把和一个类型关联起来的函数称之为方法,如下示例:
package lib
import "time"
type Person struct {
age int
name string
}
func (p Person) GetName() string {
return p.name
}
func GetTime() time.Time{
return time.Now()
}
其中GetTime()
可以通过lib.GetTime()
直接调用,称之为函数;而GetName()
则属于Person
这个结构体的函数,只能声明了Person
类型的实例后才可以调用,称之为方法。
不管是函数还是方法,定义是一摸一样的。而在这里,最可以讲的就是多值返回,也就是可以同时返回多个值,这就大大为我们带来了方便,比如上个遍历map
的例子,直接可以获取K、V
,如果只能返回一个值,我们就需要调用两次方法才可以。
func GetTime() (time.Time,error){
return time.Now(),nil
}
多值返回也很简单,返回的值使用逗号隔开即可。如果要接受多值的返回,也需要以逗号分隔的变量,有几个返回值,就需要几个变量,比如这里:
now,err:=GetTime()
如果有个返回值,我们用不到,不想浪费一个变量接收怎么办?这时候可以使用空标志符_
,这是java
没有的。
now,_:=GetTime()
指针
go
的指针和C
中的声明定义是一样的,其作用类似于Java
引用变量效果。
var age int = 10
var p *int = &age
*p = 11
fmt.Println(age)
其中指针p
指向变量age
的内存地址,如果修改*p
的值,那么变量age
的值也同时会被修改,例子中打印出来的值为11
,而不是10
.
相对应java
引用类型的变量,可以理解为一个HashMap
类型的变量,这个变量传递给一个方法,在该方法里对HashMap
修改,删除,就会影响原来的HashMap
。引用变量集合类最容易理解,自己的类也可以,不过基本类型不行,基本类型不是引用类型的,他们在方法传参的时候,是拷贝的值。
结构体替代类
go
中没有类型的概念,只有结构体,这个和C
是一样的。
type Person struct {
age int
name string
}
go
中的结构体是不能定义方法的,只能是变量,这点和Java
不一样的,如果要访问结构体内的成员变量,通过.
操作符即可。
func (p Person) GetName() string {
return p.name
}
这就是通过.
操作符访问变量的方式,同时它也是一个为结构体定义方法的例子,和函数不一样的是,在func
关键字后要执行该方法的接收者,这个方法就是属于这个接收者,例子中是Person
这个结构体。
在 go
中如果想像Java
一样,让一个结构体继承另外一个结构体怎么办?也有办法,不过在 go
中称之为组合或者嵌入。
type Person struct {
age int
name string
Address
}
type Address struct {
city string
}
结构体Address
被嵌入了Person
中,这样Person
就拥有了Address
的变量和方法,就想自己的一样,这就是组合的威力。通过这种方式,我们可以把简单的对象组合成复杂的对象,并且他们之间没有强约束关系, go
倡导的是组合,而不是继承、多态。
接口
go
的接口和Java
类型,不过它不需要强制实现,在 go
中,如果你这个类型(基本类型,结构体等都可以)拥有了接口的所有方法,那么就默认为这个类型实现了这个接口,是隐式的,不需要和java
一样,强制使用implement
强制实现。
type Stringer interface {
String() string
}
func (p Person) String() string {
return "name is "+p.name+",age is "+strconv.Itoa(p.age)
}
以上实例中可以看到,Person
这个结构体拥有了fmt.Stringer
接口的方法,那么就说明Person
实现了fmt.Stringer
接口。
接口也可以像结构体一样组合嵌套,这里不再赘述。
并发
go
并发主要靠goroutine
支持,也称之为go协程
或者go程
,他是语言层面支持的,非常轻量级的多任务支持,也可以把他简单的理解为java
语言的线程,不过是不一样的。
go run()
这就启动一个goroutine
来执行run
函数,代码非常简洁,如果在java
中,需要先New
一个Thread
,然后在重写他的run
方法,然后在start
才可以开始。
两个goroutine
可以通过channel
来通信,channel
是一个特殊的类型,也是 go
语言级别上的支持,他类似于一个管道,可以存储信息,也可以从中读取信息。
package main
import "fmt"
func main() {
result:=make(chan int)
go func() {
sum:=0
for i:=0;i<10;i++{
sum=sum+i
}
result<-sum
}()
fmt.Print(<-result)
}
以上示例使用一个单独的goroutine
求和,当得到结果时,存放在result
这个chan
里,然后供 main
goroutine
读取出来。当result
没有被存储值的时候,读取result
是阻塞的,所以会等到结果返回,协同工作,通过chan
通信。
对于并发, go
还提供了一套同步机制,都在sync
包里,有锁,有一些常用的工具函数等,和java
的concurrent
框架差不多。
异常机制
相比java
的Exception
来说, go
有两种机制,不过最常用的还是error
错误类型,panic
只用于严重的错误。
type error interface {
Error() string
}
go
内置的error
类型非常简洁,只用实现Error
方法即可,可以打印一些详细的错误信息,比如常见的函数多值返回,最后一个返回值经常是error
,用于传递一些错误问题,这种方式要比java
throw Exception
的方法更优雅。
Defer代替finally
go
中没有java
的finally
了,那么如果我们要关闭一些一些连接,文件流等怎么办呢,为此go
为我们提供了defer
关键字,这样就可以保证永远被执行到,也就不怕关闭不了连接了。
f,err:=os.Open(filename)
defer f.Close()
readAll(f)
统一编码风格
在编码中,我们有时为了是否空行,大括号是否独占一行等编码风格问题争论不休,到了 go
这里就终止了,因为 go
是强制的,比如花括号不能独占一行,比如定义的变量必须使用,否则就不能编译通过。
第二种就是go fmt
这个工具提供的非强制性规范,虽然不是强制的,不过也建议使用,这样整个团队的代码看着就像一个人写的。很多 go
代码编辑器都提供保存时自动gofmt
格式的话,所以效率也非常高。
便捷的部署
go
最终生成的是一个可执行文件,不管你的程序依赖多少库,都会被打包进行,生成一个可执行文件,所以相比java
庞大的jar
库来说,他的部署非常方便,执行运行这个可执行文件就好了。
对于Web
开发,更方便,不用安装jdk
,tomcat
容器等等这些环境,直接一个可执行文件,就启动了。对于 go
这种便捷的部署方式,我觉得他更能推进docker
的服务化,因为docker
就是倡导一个实例一个服务,而且不用各种依赖,layer
层级又没那么多,docker image
也会小很多。
最后, go
目前已经在TIOBE
语言排行榜上名列13
名了,上升速度还是非常快的,而且随着服务化,容器化,他的优势会越来越多的显现出来,得到更广泛的应用。
如果你感兴趣,那么开始吧,提前准备,机会来的时候,就不会错过了。