Go语言语法及特性总结

幸运草 2020年4月19日02:57:00前端框架评论阅读模式

Go语言是最近几年增长最快的编程语言之一,特别是云的逐渐普及更是促进了Go语言的普及。本文详细总结了Go语言的语法以及特性,这些知识也是成为一个合格的Go语言开发者必须具备的基本条件。本文主要参考了Go语言官网的Effective Go。

1、Hello World
按照惯例,以一个Hello World开始。你可以在任何你喜欢的编辑器中输入下面的代码,保存成HelloWorld.go文件(当然,你也可以取其它的名字,但后缀必须是.go)。零君目前使用的IDE是Visual Studio Code。

package main

import "fmt"

func main() {

fmt.Println("Hello World")

}

Go与C、Java一样,程序的入口点也是main函数。"fmt"是Go语言的一个标准包(package)。

运行上面的代码之前,需要先配置好go语言环境。如下两步即可:

1、首先下载适合本地平台的go语言包,安装。然后配置环境变量GOROOT指向go语言的安装目录。

2、配置环境变量GOPATH。GOPATH设置的是Go语言的工作目录,可以设置成任意目录。${GOPATH}里面包含三个子目录,分别是src、pkg、bin。src是存放source code的地方,pkg和bin则是存放编译产生的二进制文件。

将上面HelloWorld.go放到如下地方:

${GOPATH}/src/example/HelloWorld.go

然后执行下面的命令,就会看到期望的"Hello World",

cd ${GOPATH}/src/example

go run HelloWorld.go

2、编码风格

编码风格很大程度上决定了代码可读性,特别是一个团队共同开发维护一个项目时,如果不同的人采用不同的风格,比如不同的缩进,那么每个人阅读别人的代码时就会很不习惯。Go语言提供了一个工具gofmt处理这些问题,例如使用如下命令格式化上面的HelloWorld.go,

go fmt HelloWorld.go

也可以直接对一个package里面所有的源文件同时格式化,

go fmt github.com/coreos/etcd/client

这里要特别提两点:

(1) Go语言里面缩进是使用Tab;

(2) 当代码中存在多个连续的空行,gofmt格式化之后会保留一个空行。如果你无意中在某个地方按了几次回车,而你压根就不期望在那里出现空行,那么gofmt之后最终还是会保留一个空行;这时你就需要手动删除这个多余的空行。

Go语言既支持/* xxx */式的跨行的注释,也支持//单行注释。例如:

/*

Package test is a example for demo.

You can update the demo yourself.

*/

// Package test is a example for demo.

// You can update the demo yourself

对于命名规则,Go语言推荐驼峰式,而不建议下划线式,例如"softLink"、“SoftLink”是推荐的名字,而"soft_Link","Soft_Link"则不是。

用过C++或Java的人都熟悉Getter/Setter函数,以Java为例,比如有一个成员变量“int score”,那么可以为这个变量定义两个函数如下:

public int GetScore() {

return score

}

public void SetScore(int score) {

this.score = score

}

Go语言虽然不是面向对象的开发语言,但同样可以为某个类型定义Getter/Setter方法,但是Go语言对于Getter方法的命令习惯不同。还是上面的例子,Go语言推荐的Getter方法名是Score,而不是GetScore。Setter方法相同。当然,如果将变量score直接命名为Score,那么就不需要对应的Getter/Setter方法了。

在Go语言中,任何命名的首字母如果是大写,那么该变量或函数(或方法)在包外面就是可见的,类似于C++/Java中的public修饰符。反之,如果是小写的,那么只能在包内使用。

3、控制结构

像其它语言一样,Go也有if、for、switch等控制结构。除此之外,Go还支持基于类型的switch,以及多路通讯选择select。

声明与赋值

Go语言中的变量既可以显示声明类型,也可以不用显示声明类型。例如下面就是显示声明了一个int类型的变量:

var i int

而下面的例子中就没有显示声明变量的类型,因为rand.Intn的返回类型是int,所以变量v的类型也就是int。注意这里操作符是:=,表示声明一个新的变量并给这个变量赋值。如果操作符是=,那仅仅是给变量赋值,这时如果变量之前没有声明过,就会编译失败。

v := rand.Intn(10)

Go语言与C、Java的一个不同,就是Go的函数或方法可以返回多个值,例如:

f, err := os.Open(filename)

if err != nil {

return err

}

如果文件成功打开,则f就是指向新打开文件的File指针,err则为nil;如果打开失败,err就对应具体的错误。这是Go语言里面错误处理方式。

上面的例子同时声明了两个新的变量,f和err。如果将上面的代码改成如下,也是合法的。但是第二次打开文件的那条语句,只是声明了变量f2;由于err之前声明过,所以这里只是重新赋值。对于这种只有部分变量声明的情况,操作符也必须用:=,否则就会编译错误。

f1, err := os.Open(filename)

if err != nil {

return err

}

f2, err := os.Open(filename)

if err != nil {

return err

}

if条件

if条件后面的语句必须用大括号括起来,哪怕里面只有一条语句,例如:

if i < 0 {

fmt.Println("i<0")

} else if i >= 0 && i <= 10 {

fmt.Println("0-10")

} else {

fmt.Println("i>10")

}

但是条件判断是不需要用小括号括起来的(当然,括起来也不会报错)。

if条件可以有初始化语句,如下例所示。注意初始化语句和判断语句之前用分号';'隔开。

if v:=rand.Intn(10); v < 5 {

fmt.Println("v <5")

}

看到这里,你可以发现if条件后面紧跟着的 '{' 都是在同一行的结尾。在Java或者C++中,这个'{'位置比较随意。但是在Go语言中,强制必须在同一行的结尾,否则编译会抱错。这条规则适用于if、for、switch、select以及函数或方法体的第一个'{'。之所以会有这样的强制要求,原因在于Go语言编译器自动插入分号‘;’分隔不同的语句。

Go编译器与C/Java类似,也是使用分号‘;’分隔不同的语句,但是区别在于Go不需要程序员输入分号,而是编译器自动插入了分号。只有if、for里面有初始化语句时,才需要程序员手动输入分号分隔初始化语句和条件语句(for还有继续语句)。Go编译器插入分号的规则是:当行结尾的token是下列情形时,就会在行结尾插入一个分号:

a) 结尾的token是一个类型标识符,比如int;

b) 结尾的token是一个常量,比如数字,或字符串常量;

c) 是下列任何一种token:

break continue fallthrough return ++ -- ) }

回到之前说到的强制要求(以if为例),如果将紧跟着if条件的大括号‘{’写到了下一行,如下例所示。那么Go编译器就会在"if i<0"后面插入一个分号,就会产生非预期的结果。

if i < 0

{

fmt.Println("i<0")

}

for循环

Go语言中没有do、while循环,只有for循环。下面通过几个例子说明。

for i:=0; i<3; i++ {

fmt.Println(i)

}

上面的例子也可以改写为:

i:=0

for i<3 {

fmt.Println(i)

i++

}

这里要注意一点,i++以及i--是语句(statement),而不是表达式(expression)。所以下面的语句是非法的。

v1 := myArray[i++]

v2 := myArray[i++]

for循环也可以遍历array、slice、map、string、channel,这些概念下面会提到。主要是通过关键字range来实现的。这里以map为例,我们都知道map就是key、value对的集合。下面的例子就是输出所有的key/value对的值。

for key, value := range myMap {

fmt.Printf("key = %s, value = %sn", key, value)

}

如果只需要输出key的值,那么上面的例子可以改写为:

for key := range myMap {

fmt.Printf("key = %sn", key)

}

如果只需要输出value的值,则可以改写为:

for _, value := range myMap {

fmt.Printf("value = %sn", value)

}

上面的'_'是blank identifier,此处的作用是忽略key的值。在Go语言中,blank identifer还是用途比较大的,这里就不多介绍了。

switch

Go语言中的Switch比C/C++/Java中的更灵活。switch后面的表达式可以不是常量;cases分支从上至下执行,直到遇到一个匹配成功的为止。

例如下面的例子其实与if-else if-else的作用是相同的。

switch {

case j < 0:

fmt.Println("case i<0")

case j>=0 && j<=5:

fmt.Println("case j>0 && j<=5")

default:

fmt.Println("Last case")

}

注意switch中的case之间是不会fall-through的,但是可以将多个case用逗号合并。如下例所示:

switch k {

case 1, 2, 3:

fmt.Println("less than 3")

case 4,5:

fmt.Println("less than 5")

default:

fmt.Println("greater than 5")

}

Go中的switch还有一种特殊的用法,那就是用于动态发现变量的类型,如下例所示。用Go语言的术语这种用法就是Type Switch。

var v interface{}

v = SomeFunc()

switch v.(type) {

case int:

fmt.Println("Integer")

case string:

fmt.Println("string")

default:

fmt.Println("Unknown")

}

上面的例子通过另一种称为type assertion的方式改写如下:

var v interface{}

v = SomeFunc()

if _, ok := v.(int); ok {

fmt.Println("Integer")

} else if _, ok := v.(string); ok{

fmt.Println("String")

} else {

fmt.Println("Unknown")

}

select

与switch类似,select也是一种多路选择器,但区别在于select只针对I/O操作。示例如下:

chan1 := make(chan int, 1)

chan2 := make(chan int, 1)

select {

case i := <-chan1:

fmt.Printf("data read from chan1: %dn", i)

case i := <-chan2:

fmt.Printf("data read from chan1: %dn", i)

default:

fmt.Println("no data available")

}

上面的例子中用到了channel,下面会具体说明。

4、数据结构

Go语言常用的数据结构有Array、slice、map。

内存分配

谈到数据结构,首先要知道如何分配内存。Go提供了两个内置的函数new和make来分配内存。先来看一个new的例子:

type Example struct {

val1 int

val2 string

}

obj := new(Example)

对于new,要注意两点:(1) new返回的是一个指针,所以上面例子obj的类型是Example指针;(2) new只是分配内存,并不会初始化,new只是将分配的内存置0,所以上面new创建的Example中val1的值是0,而val2是一个空字符串。

make则不仅分配内存,还初始化内存。但是make只能创建slice、map、channel这三种数据类型。示例如下:

s := make([]int, 2, 10)

上面的例子就是分配了一个int类型的slice,这个slice的初始长度为2,容量为10。注意:make返回的不是一个指针,而是一个slice结构。每个slice结构包含三个元素:指向数据内存的指针、长度、容量。具体到上面的例子,实际上先创建了一个具有10个元素的int数组,然后创建一个slice结构,其中slice结构的长度为2,容量为10,而slice结构的指针就指向数组的前两个元素。

注:slice的长度可以动态变化

很多时候,也可以直接用下面的方式创建并初始化结构体:

v := Example{val1: 5, val2: "abc"}

或者定义成指针:

v := &Example{val1: 5, val2: "abc"}

Array

Array就是数组,与其他语言中的数组用法类似。例如下面就创建了一个长度为3的int数组:

v := [3]int{1,2,3}

注意几点:

(1) 当一个array赋值给另一个array时,会拷贝array中所有的元素,两个array会对应不同的内存区域。同样,当函数的参数是array时,将一个array传给函数会拷贝所有的元素。

(2) array的长度是类型的一部分,例如[10]int和[20]int是两种不同的类型。

通常情况,Array很少使用,一般是使用Slice。

Slice

Slice其实是对Array的封装及扩展,从而变得更通用、功能更强大。前面已经提到过,slice结构中包含一个指向底层Array的指针,所以当你把一个Slice赋值给另一个slice时,那么这两个slice会指向相同的内存区域。前面在内存分配小节中已经对slice举例说明过了,这里就不再重复举例了。

虽然Slice包含指向底层Array的指针,但slice的长度可以动态扩展。当底层Array的空间不足时,Slice会创建新的容量更大的Array,拷贝所有数据,然后指向新的Array。

我们也可以定义二维Array或者二维Slice,例如:

type array1 [3][3]int   // 2-dimension array

type slice1 [][]int        // 2-dimension slice

二维Slice也就是每个元素也是一个Slice。由于Slice是变长的,所以二维Slice中,每个内部Slice的长度可以不同。

Map

Map就是key/value对的集合,是Go语言提供的一种内置的数据结构。例如下面定义的map,key是string,而value则是int。

var timeMap = map[string]int {

"SECOND": 1,

"MINUTE": 60,

"HOUR": 60*60,

"DAY": 60*60*24,

}

一般通过前面提过的range来遍历map:

for k, v := range timeMap {

fmt.Printf("%s, %dn", k, v)

}

注意用某个key直接从map中读取某个value时,会返回一个bool变量,来标示对应的key是否存在,示例如下:

if v, ok := timeMp["HOUR"]; ok {

fmt.Println(v)

} else {

fmt.Println("Not found")

}

5、函数与方法

Go语言中的函数与C/Java中的函数一个重要的不同是,Go的函数可以返回多个值,上面也已经提过了。但是值得一提的是,Go中可以对返回值命名,在函数体内可以象其它变量一样操作返回值变量。这样带来的好处是函数返回时,只需要一个return命令即可,后面无需再提供返回的值,示例如下:

func TestFunc1() (retValue1, retValue2 int) {

retValue1 = 2

retValue2 = 3

return

}

在其它地方可以调用这个函数:

v1, v2 := TestFunc()

fmt.Printf("%d, %dn",  v1,v2)

Go中有一个特殊的关键字defer,它的作用是延迟函数的执行时间。下面的例子中,对f.Close()的调用并不会马上执行,而是在函数TestFunc返回时才执行。这样做有两个好处:1、不管TestFunc函数执行过程中从什么路径返回,f.Close()最后都会被执行;2、将f.Close()与os.Open()放在一起,使代码更清晰更易维护。

func TestFunc(name string) {

f, err := os.Open(name)

if err != nil {

fmt.Printf("Failed to open file %s, err: %vn", name, err)

return

}

defer f.Close()

......

}

但有一点一定要注意,defer虽然会推迟函数的执行时间,但如果执行的函数有参数,那么参数的计算不会推迟。例如下面的例子,函数SampleFunc有一个输入参数,取当前的系统时间。这个时间的计算是defer语句执行的时候就确定了,而不是SampleFunc函数执行的时候计算的。

defer SampleFunc(time.Now())

值得一提的是,可以为每个源代码文件中定义init函数,该函数会在该文件加载时自动执行。可以在init函数中做一些初始化或者校验的操作。init函数是在包中所有变量都初始化完成之后才执行的。如果还import了其它的包,那么import的包的初始化最先实施。

Go中的方法(Method)和函数(Function)是两个不同的概念。当一个函数与某种数据类型绑定时,就是方法。下面的例子中TestFunc就是一个方法,它与类型Example绑定:

type Example struct {

val1 int

val2 string

}

func (p *Example) TestFunc() {

p.val1 = 11

p.val2 = "abc"

}

在其它地方可以调用这个方法,示例如下,obj就是Example类型的指针,obj就称为接收器(receiver)。

obj := &Example{}

obj.TestFunc()

fmt.Printf("%d, %s", obj.val1, obj.val2)

虽然Go不是一种面向对象的开发语言,但通过上面的例子可以看出,将数据与操作结合在一起,Go同样具有面向对象的思维。

上面的例子中因为方法TestFunc对应的receiver是指针类型,所以方法体内对receiver内的数据做的修改,对调用者是可见的,所以上面的例子调用者最后输出的内容为:

11, abc

但是如果将方法TestFunc对应的receiver定义为非指针的形式(如下),那么在方法体内所作的修改,对于调用者是不可见的。

func (p Example) TestFunc() {

p.val1 = 11

p.val2 = "abc"

}

6. 接口

前面提到的方法(method)因为将数据和操作绑定在了一起,所以Go也吸收了面向对象的思想。Go体现面向对象的另一个重要因素在于接口(Interface)。Go语言的接口与Java的Interface本质上是相同的,都是对行为的抽象。但是在具体用法上差异还是挺大的。

例如Go语言的io包中定义的Reader和Writer就是两个接口,

type Reader interface {

Read(p []byte) (n int, err error)

}

type Writer interface {

Write(p []byte) (n int, err error)

}

任何命名的类型都可以实现上面两个接口。只要为某种类型(指针和interface类型除外)绑定的方法中,包含某个接口中定义的方法,那么该类型就实现这个接口。例如下面的例子,Example就实现了io.Reader接口。

type Example struct {

val1 int

val2 string

}

func (p *Example) Read(p []byte) (n int, err error){

......

}

一个类型也可以实现多个接口,例如我们还可以为Example实现io.Writer接口,只要再绑定一个具有相同签名的Write方法即可。

还有一种常见的用法就是联合多个接口,从而形成一个新的接口,例如io.ReadWriter就是上面的io.Reader和io.Writer的联合。

type ReadWriter interface {

Reader

Writer

}

7、并发

C++/Java中对于多线程并发的处理比较复杂,多个线程之间通过共享内存的方式来交互,这时对于共享数据的保护就很重要,这样就需要引入锁等同步机制,很容易导致新的问题。而Go则通过完全不同的途径解决了并发的问题。在Go中,多个线程(准确的说是go routine)之间无需共享内存,多个go routine之间是通过channel来交换数据。这里引用Go官方的一句口号:

Do not communicate by sharing memory; instead, share memory by communicating.

不要通过共享内存来通信;而是应该通过通信来共享内存。

Go引入了Goroutine概念。Goroutine相对于thread来说更轻量级。任意一个函数或方法的并发执行都是一个goroutine。通过关键字go来执行任何函数或方法,就会启动一个新的goroutine来执行那个函数或方法。例如下面的语句会启动一个新的goroutine来执行函数TestFunc。当函数执行结束,goroutine就会退出。

go TestFunc()

Channel是Go语言中一个非常重要的概念,Goroutine之间就是通过channel通信的,channel是一个很大的话题,这里只是通过一个完整的简单的例子来说明,如下:

package main

import (

"fmt"

"time"

)

func TestFunc(c chan string) {

for {

str := <- c

fmt.Printf("Received: %sn", str)

if str == "exit" {

fmt.Println("bye")

break

}

}

}

func main() {

ch := make(chan string, 1)

go TestFunc(ch)  // start a new goroutine to execute TestFunc()

for i:=0; i<3; i++ {

ch <- "hello"

time.Sleep(1*time.Second)

}

ch <- "exit"

time.Sleep(1*time.Second)

fmt.Println("main exiting...")

}

上面例子中,在main函数里,首先用内置函数make创建一个channel,第二个参数1表示channel里可以缓存一个字符串。如果是0的话,则表示非缓存的channel;那么如果没有Goroutine从channel中读取的时候,其他Goroutine试图往channel里写入的时候会block住。TestFunc()不断的从channel里读取数据,然后直接输出。如果读取到的是"exit",那么就退出,否则就继续读取。而main函数往channel里每隔1秒写入一个"hello",连续写入三个"hello"之后,就写入一个"exit",通知TestFunc退出。程序的输出如下:

Received: hello

Received: hello

Received: hello

Received: exit

bye

main exiting...

8、错误处理

Go语言中函数或方法可以返回多个值,所以一般情况下,函数或方法会返回一个error对象,用来表示具体发生的错误。如果没有任何错误发生,返回的error对象就是nil(相当于Java里的null)。在前面也已经举例说明过了。

Go语言定义了一个内置的error interface,如下:

type error interface {

Error() string

}

Go的SDK中的各种Error类型都实现了这个接口,比如os.PathError。我们自己也可以定义各种应用相关的Error。只要实现了上面这个error接口即可,其实也就是实现Error()方法。

panic和recover是Go语言提供两个内置的函数。panic相当于Java里面的异常,当程序发生了比较严重的错误的时候,就可以调用panic,相当于抛出一个异常,程序立即停止执行,并开始沿着调用栈一层层向调用者返回(unwinding),如果没有任何地方处理,那么程序最后会退出,并输出调用栈信息。我们可以在返回的过程中,在某处调用了recover来处理这种异常情况。 下面是一个完整的例子来说明panic和recover的用法:

package main

import "fmt"

func doSomething(name string) {

if name == "" {

panic("empty name")

} else {

fmt.Println(name)

}

}

func panicTest() {

defer func() {

if err := recover(); err != nil {

fmt.Println("doSomething failed:", err)

}

}()

doSomething("")

}

func main() {

panicTest()

}

通常panic意味着不可恢复的错误,让程序退出有时是安全的做法。所以谨慎使用recover。

9、垃圾回收

Go语言提供了垃圾回收的机制,也就意味着与Java一样,将开发人员从内存管理的噩梦中解放出来了。垃圾回收是个很大的话题,将来有机会会单独深入分析。

10、结束语

如果耐心阅读到这里,就会发现Go语言的特点(或者说优点):

(1) 虽然不是面向对象的语言,但是吸收了面向对象的思想;

(2) 提供接口这样抽象的概念;

(3) 支持指针;

(4) 提供了垃圾回收机制;

(5) 用package来管理代码,与Java类似;

(6) 提供了一个reflection包,可以在运行时动态获取Value和Type信息(限于篇幅,本文正文中没有涉及到reflection);

(7) defer提供的延迟执行特性,可以很方便的释放资源;

(8) 灵活的错误处理机制;没有异常,也没有try-catch-finally,不过panic类似于Java的异常;

(9) 函数可以返回多个值,通常会返回一个error对象;

(10) Goroutine更轻量级,提供并发支持;

(11) 侧重通过channel通信,而不是共享内存;

(12) 提供了gofmt工具来格式化source code。

本文对Go语言进行了系统的总结,尽量涵盖了Go语言了方方面面的特性。但任何一个方面都值得进一步的深入分析,比如垃圾回收等。以后有时间可能还会挑选一些点深入分析。

特别声明:以上文章内容仅代表作者本人观点,不代表变化吧观点或立场。如有关于作品内容、版权或其它问题请于作品发表后的30日内与变化吧联系。

  • 赞助本站
  • 微信扫一扫
  • weinxin
  • 加入Q群
  • QQ扫一扫
  • weinxin
幸运草
Go语言接口规则 前端框架

Go语言接口规则

Go语言接口规则 接口是一个或多个方法签名的集合。任何类型的方法集中只要拥有该接口对应的全部方法签名。就表示它 "实现" 了该接口,无须在该类型上显式声明实现了哪个接口。对应方法,是指有相同名称、参数...
Go语言中处理 HTTP 服务器 前端框架

Go语言中处理 HTTP 服务器

1 概述 包 net/http 提供了HTTP服务器端和客户端的实现。本文说明关于服务器端的部分。 快速开始: package main import (   "log"   "net/http" )...

发表评论