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