作者:京东科技 韩国凯

前言

Go语言定义

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种动态、强类型、编译型语言。Go 语言语法与 C 相近,但性能上有:内存平安,GC,构造状态及 CSP-style 并发计算

适用范围

本篇文章实用于学习过其余面向对象语言(Java、Php),但没有学过Go语言的初学者。文章次要从Go与Java性能上的比照来论述Go语言的根底语法、面向对象编程、并发与谬误四个方面。

一、根底语法

Go语言的根底语法与惯例的编程语言根本相似,所不同的有申明变量的形式,数组、切片、字典的概念及性能与Java不太雷同,不过Java中这些数据结构都能够通过类比性能的形式在Go中应用。

1.1 变量、常量、nil与零值、办法、包、可见性、指针

1.1.1 变量申明

Go语言中有两种形式

1.应用var关键字申明,且须要留神的是,与大多数强类型语言不同,Go语言的申明变量类型位于变量名称的前面。Go语句完结不须要分号。

var num int

var result string = "this is result"

2.应用:=赋值。

num := 3 等同于 var num int = 3

其中变量的类型会依据右侧的值进行匹配,例如"3"会匹配为int,"3.0"会匹配为float64,"result"会匹配为string。

1.1.2 常量申明

应用const来申明一个常量,一个常量在申明后不可扭转。

const laugh string = "go"

1.1.3 nil与零值

只申明未赋值的变量,其值为nil。相似于java中的“null”

没有明确初始值的变量申明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。

1.1.4 办法、包

Go中办法的定义

应用func关键字来定义一个办法,前面跟办法名,而后是参数,返回值(如果有的话,没有返回值则不写)。

func MethodName(p1 Parm, p2 Parm) int{}

//学习一个语言应该从Hello World开始!package mainimport "fmt"func main() {    fmt.Println("Hello World!")// Hello World!    fmt.Println(add(3, 5)) //8    var sum = add(3, 5)}func add(a int, b int) int{    return a+b;}
多个返回值

Go 函数与其余编程语言一大不同之处在于反对多返回值,这在处理程序出错的时候十分有用。例如,如果上述 add 函数只反对非负整数相加,传入正数则会报错。

//返回值只定义了类型 没有定义返回参数func add(a, b int) (int, error) {    if a < 0 || b < 0 {        err := errors.New("只反对非负整数相加")        return 0, err    }    a *= 2    b *= 3    return a + b, nil}//返回值还定义了参数 这样能够间接return 并且定义的参数能够间接应用 return时只会返回这两个参数func add1(a, b int) (z int, err error) {    if a < 0 || b < 0 {        err := errors.New("只反对非负整数相加")        return   //理论返回0 err 因为z只定义没有赋值 则nil值为0    }    a *= 2    b *= 3    z = a + b    return //返回 z err}func main()  {    x, y := -1, 2    z, err := add(x, y)    if err != nil {        fmt.Println(err.Error())        return    }    fmt.Printf("add(%d, %d) = %d\n", x, y, z)}
变长参数
func myfunc(numbers ...int) {    for _, number := range numbers {        fmt.Println(number)    }}slice := []int{1, 2, 3, 4, 5}//应用...将slice打碎传入myfunc(slice...)
包与可见性

在 Go 语言中,无论是变量、函数还是类属性和成员办法,它们的可见性都是以包为维度的,而不是相似传统面向编程那样,类属性和成员办法的可见性封装在所属的类中,而后通过 privateprotected 和 public 这些关键字来润饰其可见性。

Go 语言没有提供这些关键字,不论是变量、函数,还是自定义类的属性和成员办法,它们的可见性都是依据其首字母的大小写来决定的,如果变量名、属性名、函数名或办法名首字母大写,就能够在包外间接拜访这些变量、属性、函数和办法,否则只能在包内拜访,因而 Go 语言类属性和成员办法的可见性都是包一级的,而不是类一级的。

如果说一个名为domain的文件夹下有3个.go文件,则三个文件中的package都应为domain,其中程序的入口main办法所在的文件,包为main

//定义了此文件属于 main 包package main//通过import导入标注库中包import "fmt"func main() {    fmt.Println("Hello World!")// Hello World!    fmt.Println(add(3, 5)) //8    var sum = add(3, 5)}func add(a int, b int) int{    return a+b;}

1.1.5 指针

对于学过C语言来说,指针还是比拟相熟的,我所了解的指针,其实就是一个在内存中理论的16进制的地址值,援用变量的值通过此地址去内存中取出对应的实在值。

func main() {    i := 0    //应用&来传入地址    fmt.Println(&i) //0xc00000c054        var a, b int = 3 ,4    //传入 0xc00000a089 0xc00000a090    fmt.Println(add(&a, &b)) }//应用*来申明一个指针类型的参数与应用指针func add(a *int, b *int)int{    //接管到 0xc00000a089 0xc00000a090    //返回 0xc00000a089地位查找具体数据 并取赋给x    x := *a    //返回 0xc00000a090地位查找具体数据 并取赋给y    y := *b    return x+y}

1.2 条件、循环、分支

1.2.1 条件

与Java语言的if基本相同

// ifif condition {     // do something }// if...else...if condition {     // do something } else {    // do something }// if...else if...else...if condition1 {     // do something } else if condition2 {    // do something else } else {    // catch-all or default }

1.2.2 循环

sum := 0 //一般for循环for i := 1; i <= 100; i++ {     sum += i }//有限循环for{    sum++    if sum = 100{        break;    }}//带条件的循环for res := sum+1; sum < 15{    sum++    res++}//应用kv循环一个map或一个数组  k为索引或键值 v为值 k、v不须要时能够用_带替for k, v := range a {    fmt.Println(k, v)}

1.2.3 分支

score := 100switch score {case 90, 100:    fmt.Println("Grade: A")case 80:    fmt.Println("Grade: B")case 70:    fmt.Println("Grade: C")case 65:    fmt.Println("Grade: D")default:    fmt.Println("Grade: F")}

1.3 数组、切片、字典

1.3.1 数组

数组性能与Java语言相似,都是长度不可变,并且能够应用多维数组,也能够通过arrays[i]来存储或获取值。

//申明var nums [3]int //申明并初始化var nums = [3]int{1,2,3} <==> nums:=[3]int{1,2,3}//应用for sum := 0, i := 0;i<10{    sum += nums[i]    i++}//批改值num[0] = -1

数组应用较为简单,然而存在着难以解决的问题:长度固定 。

例如当咱们在程序中须要一个数据结构来存储获取到的所有用户,因为用户数量是会随着工夫变动的,然而数组其长度却不可扭转,所以数组并不适宜存储长度会产生扭转的数据。因而在Go语言中通过应用切片来解决以上问题。

1.3.2 切片

切片相比于Java来说是一种全新的概念。在Java中,对于不定长的数据存储构造,能够应用List接口来实现操作,例如有ArrayList与LinkList,这些接口能够实现数据的随时增加与获取,并没有对长度进行限度。然而在Go中不存在这样的接口,而是通过切片(Slice)来实现不定长的数据长度存储

切片与数组最大的不同就是切片不必申明长度。然而切片与数组并非毫无关系,数组能够看作是切片的底层数组,而切片则能够看作是数组某个间断片段的援用。切片能够只应用数组的一部分元素或者整个数组来创立,甚至能够创立一个比所基于的数组还要大的切片:

长度、容量

切片的长度就是它所蕴含的元素个数。

切片的容量是从它的第一个元素开始数,到其底层数组元素开端的个数。

切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

切片的长度从性能上类比与Java中List的size(),即通过len(slice)来感知切片的长度,即可对len(slice)进行循环,来动态控制切片内的具体内容。切片的容量在理论开发中使用不多,理解其概念即可。

创立切片
//申明一个数组var nums =[3]int{1, 2, 3}//0.间接申明var slice =[]int{0, 1, 2}//1.从数组中援用切片 其中a:b是指包含a但不包含bvar slice1 = nums[0:2] //{1,2}//如果不写的则默认为0(右边)或最大值(左边)var slice2 = slice1[:2] <==> var slice2 = slice1[0:] <==>var slice2 = slice1[:]//2.应用make创立Slice 其中int为切片类型,4为其长度,5为容量slice3 := make([]int, 5)slice4 := make([]int, 4, 5)
动静操作切片
//应用append向切片中动静的增加元素func append(s []T, vs ...T) []Tslice5 := make([]int, 4, 5) //{0, 0, 0, 0}slice5 = append(slice5, 1) //{0,0,0,0,1}//删除第一个0sliece5 = slice5[1:]
切片的罕用场景

模仿上述提到的问题应用切片解决方案

//申明切片var userIds = []int{}//模仿获取所有用户IDfor i := 0; i< 100{    userIds = append(userIdS, i);    i++;}//对用户信息进行解决for k,v := range userIds{    userIds[k] = v++}

1.3.3 字典

字典也可称为 ‘键值对’ 或 ‘key-value’,是一种罕用的数据结构,Java中有各种Map接口,罕用的有HashMap等。在Go中通过应用字典来实现键值对的存储,字典是无序的,所以不会依据增加程序来保证数据的程序。

字典的申明与初始化
//string为键类型,int为值类型maps := map[string]int{  "java" : 1,  "go" : 2,  "python" : 3,}//还能够通过make来创立字典 100为其初始容量 超出可扩容maps = make(map[string]int, 100)
字典的应用场景
//间接应用fmt.Println(maps["java"]) //1//赋值maps["go"] = 4//取值 同时判断map中是否存在该键 ok为bool型value, ok := maps["one"] if ok { // 找到了  // 解决找到的value }//删除delete(testMap, "four")

二、面向对象编程

2.1 Go语言中的类

家喻户晓,在面向对象的语言中,一个类应该具备属性、构造方法、成员办法三种构造,Go语言也不例外。

2.1.1 类的申明与初始化

Go语言中并没有明确的类的概念,只有struct关键字能够从性能上类比为 面向对象语言中的“类” 。比方要定义一个学生类,能够这么做:

type Student struct {    id int    name string    male bool    score float64}//定义了一个学生类,属性有id name等,每个属性的类型都在其前面//定义学生类的构造方法func NewStudent(id uint, name string, male bool, score float64) *Student {    return &Student{id, name, male, score}}//实例化一个类对象student := NewStudent(1, "学院君", 100)fmt.Println(student)

2.1.2 成员办法

Go中的成员办法申明与其余语言不大雷同。以Student类为例,

//在办法名前,增加对应的类,即可认为改办法为该类的成员办法。func (s Student) GetName() string  {    return s.name}//留神这里的Student是带了*的 这是因为在办法传值过程中 存在着值传递与援用传递 即指针的概念 当应用值传递时 编译器会为该参数创立一个正本传入 因而如果对正本进行批改其实是不失效的 因为在执行完此办法后该正本会被销毁 所以此处应该是用*Student 将要批改的对象指针传入 批改值能力起作用func (s *Student) SetName(name string) {    //这里其实是应该应用(*s).name = name,因为对于一个地址来说 其属性是没意义的 不过这样应用也是能够的 因为编译器会帮咱们主动转换    s.name = name}

2.2 接口

接口在 Go 语言中有着至关重要的位置,如果说 goroutine 和 channel 是撑持起 Go 语言并发模型的基石,那么接口就是 Go 语言整个类型零碎的基石。Go 语言的接口不单单只是接口,上面就让咱们一步步来摸索 Go 语言的接口个性。

2.2.1 传统侵入式接口实现

和类的实现类似,Go 语言的接口和其余语言中提供的接口概念齐全不同。以 Java、PHP 为例,接口次要作为不同类之间的契约(Contract)存在,对契约的实现是强制的,体现在具体的细节上就是如果一个类实现了某个接口,就必须实现该接口申明的所有办法,这个叫「履行契约」:

// 申明一个'iTemplate'接口interface iTemplate{    public function setVariable($name, $var);    public function getHtml($template);}// 实现接口// 上面的写法是正确的class Template implements iTemplate{    private $vars = array();    public function setVariable($name, $var)    {        $this->vars[$name] = $var;    }    public function getHtml($template)    {        foreach($this->vars as $name => $value) {            $template = str_replace('{' . $name . '}', $value, $template);        }        return $template;    }}

这个时候,如果有另外有一个接口 iTemplate2 申明了与 iTemplate 齐全一样的接口办法,甚至名字也叫 iTemplate,只不过位于不同的命名空间下,编译器也会认为下面的类 Template 只实现了 iTemplate 而没有实现 iTemplate2 接口。

这在咱们之前的认知中是天经地义的,无论是类与类之间的继承,还是类与接口之间的实现,在 Java、PHP 这种单继承语言中,存在着严格的层级关系,一个类只能间接继承自一个父类,一个类也只能实现指定的接口,如果没有显式申明继承自某个父类或者实现某个接口,那么这个类就与该父类或者该接口没有任何关系。

咱们把这种接口称为侵入式接口,所谓「侵入式」指的是实现类必须明确申明本人实现了某个接口。这种实现形式尽管足够明确和简单明了,但也存在一些问题,尤其是在设计标准库的时候,因为规范库必然波及到接口设计,接口的需求方是业务实现类,只有具体编写业务实现类的时候才晓得须要定义哪些办法,而在此之前,规范库的接口就曾经设计好了,咱们要么依照约定好的接口进行实现,如果没有适合的接口须要本人去设计,这里的问题就是接口的设计和业务的实现是拆散的,接口的设计者并不能总是预判到业务方要实现哪些性能,这就造成了设计与实现的脱节。

接口的过分设计会导致某些申明的办法实现类齐全不须要,如果设计的太简略又会导致无奈满足业务的需要,这的确是一个问题,而且脱离了用户应用场景探讨这些并没有意义,以 PHP 自带的 SessionHandlerInterface 接口为例,该接口申明的接口办法如下:

SessionHandlerInterface {    /* 办法 */    abstract public close ( void ) : bool    abstract public destroy ( string $session_id ) : bool    abstract public gc ( int $maxlifetime ) : int    abstract public open ( string $save_path , string $session_name ) : bool    abstract public read ( string $session_id ) : string    abstract public write ( string $session_id , string $session_data ) : bool}

用户自定义的 Session 管理器须要实现该接口,也就是要实现该接口申明的所有办法,然而理论在做业务开发的时候,某些办法其实并不需要实现,比方如果咱们基于 Redis 或 Memcached 作为 Session 存储器的话,它们本身就蕴含了过期回收机制,所以 gc 办法基本不须要实现,又比方 close 办法对于大部分驱动来说,也是没有什么意义的。

正是因为这种不合理的设计,所以在编写 PHP 类库中的每个接口时都须要纠结以下两个问题(Java 也相似):

  1. 一个接口须要申明哪些接口办法?
  2. 如果多个类实现了雷同的接口办法,应该如何设计接口?比方下面这个 SessionHandlerInterface,有没有必要拆分成多个更细分的接口,以适应不同实现类的须要?

接下咱们来看看 Go 语言的接口是如何防止这些问题的。

2.2.2 Go 语言的接口实现

在 Go 语言中,类对接口的实现和子类对父类的继承一样,并没有提供相似 implement 这种关键字显式申明该类实现了哪个接口,一个类只有实现了某个接口要求的所有办法,咱们就说这个类实现了该接口

例如,咱们定义了一个 File 类,并实现了 Read()Write()Seek()Close() 四个办法:

type File struct {     // ...}func (f *File) Read(buf []byte) (n int, err error) func (f *File) Write(buf []byte) (n int, err error) func (f *File) Seek(off int64, whence int) (pos int64, err error) func (f *File) Close() error

假如咱们有如下接口(Go 语言通过关键字 interface 来申明接口,以示和构造体类型的区别,花括号内蕴含的是待实现的办法汇合):

type IFile interface {     Read(buf []byte) (n int, err error)     Write(buf []byte) (n int, err error)     Seek(off int64, whence int) (pos int64, err error)     Close() error }type IReader interface {     Read(buf []byte) (n int, err error) }type IWriter interface {     Write(buf []byte) (n int, err error) }type ICloser interface {     Close() error }

只管 File 类并没有显式实现这些接口,甚至基本不晓得这些接口的存在,然而咱们说 File 类实现了这些接口,因为 File 类实现了上述所有接口申明的办法。当一个类的成员办法汇合蕴含了某个接口申明的所有办法,换句话说,如果一个接口的办法汇合是某个类成员办法汇合的子集,咱们就认为该类实现了这个接口。

与 Java、PHP 绝对,咱们把 Go 语言的这种接口称作非侵入式接口,因为类与接口的实现关系不是通过显式申明,而是零碎依据两者的办法汇合进行判断。这样做有两个益处:

  • 其一,Go 语言的规范库不须要绘制类库的继承/实现树图,在 Go 语言中,类的继承树并无意义,你只须要晓得这个类实现了哪些办法,每个办法是干什么的就足够了。
  • 其二,定义接口的时候,只须要关怀本人应该提供哪些办法即可,不必再纠结接口须要拆得多细才正当,也不须要为了实现某个接口而引入接口所在的包,接口由应用方按需定义,不必当时设计,也不必思考之前是否有其余模块定义过相似接口。

这样一来,就完满地防止了传统面向对象编程中的接口设计问题。

三、并发与多线程

3.1 Goroutine

对于任何一个优良的语言来说,并发解决的能力都是决定其优劣的要害。在Go语言中,通过Goroutine来实现并发的解决。

func say(s string) {    fmt.Println(s)}func main() {    //通过 go 关键字新开一个协程    go say("world")    say("hello")}

Go语言中没有像Java那么多的锁来限度资源同时拜访,只提供了Mutex来进行同步操作。

//给类SafeCounter增加锁type SafeCounter struct {    v   map[string]int    mux sync.Mutex}// Inc 减少给定 key 的计数器的值。func (c *SafeCounter) Inc(key string) {    //给该对象上锁    c.mux.Lock()    // Lock 之后同一时刻只有一个 goroutine 能拜访 c.v    c.v[key]++    //解锁    c.mux.Unlock()}

3.2 Channel

多协程之间通过Channel进行通信,从性能上能够类比为Java的volatile关键字。

ch := make(chan int) 申明一个int型的Channel,两个协程之间能够通过ch进行int数据通信。

通过Channel进行数据传输。

ch <- v    // 将 v 发送至信道 ch。v := <-ch  // 从 ch 接管值并赋予 v。
package mainimport "fmt"func sum(s []int, c chan int) {    sum := 0    for _, v := range s {        sum += v    }    c <- sum // 将和送入 c}//对于main办法来说 相当于就是开启了一个协程func main() {    s := []int{7, 2, 8, -9, 4, 0}    c := make(chan int)    //通过go关键字开启两个协程 将chaneel当做参数传入    go sum(s[:len(s)/2], c)    go sum(s[len(s)/2:], c)    //通过箭头方向获取或传入信息    x, y := <-c, <-c // 从 c 中接管    fmt.Println(x, y, x+y)}

四、错误处理

4.1 error

Go 语言错误处理机制十分简单明了,不须要学习理解简单的概念、函数和类型,Go 语言为错误处理定义了一个规范模式,即 error 接口,该接口的定义非常简单:

type error interface {     Error() string }

其中只申明了一个 Error() 办法,用于返回字符串类型的谬误音讯。对于大多数函数或类办法,如果要返回谬误,根本都能够定义成如下模式 —— 将谬误类型作为第二个参数返回:

func Foo(param int) (n int, err error) {     // ...}

而后在调用返回错误信息的函数/办法时,依照如下「卫述语句」模板编写解决代码即可:

n, err := Foo(0)if err != nil {     // 错误处理 } else{    // 应用返回值 n }

十分简洁优雅。

4.2 defer

defer用于确保一个办法执行实现之后,无论执行后果是否胜利,都要执行defer中的语句。相似于Java中的try..catch..finally用法。例如在文件解决中,无论后果是否胜利,都要敞开文件流。

func ReadFile(filename string) ([]byte, error) {    f, err := os.Open(filename)    if err != nil {        return nil, err    }    //无论后果如何 都要敞开文件流    defer f.Close()    var n int64 = bytes.MinRead    if fi, err := f.Stat(); err == nil {        if size := fi.Size() + bytes.MinRead; size > n {            n = size        }    }    return readAll(f, n)}

4.3 panic

Go语言中没有太多的异样类,不像Java一样有Error、Exception等谬误类型,当然也没有try..catch语句。

Panic(恐慌),象征在程序运行中呈现了谬误,如果该谬误未被捕捉的话,就会造成零碎解体退出。例如一个简略的panic:a := 1/0

就会引发panic: integer divide by zero。

其中第一行示意出问题的协程,第二行是问题代码所在的包和函数,第三行是问题代码的具体位置,最初一行则是程序的退出状态,通过这些信息,能够帮忙你疾速定位问题并予以解决。

4.4 recover

当有能够预感的谬误时,又不心愿程序解体退出,能够应用recover()语句来捕捉未解决的panic。recover该当放在defer语句中,且该语句应该在办法中前部,防止未能执行到defer语句时就引发了零碎异样退出。

package mainimport (    "fmt")func divide() {    //通过defer,确保该办法只有执行结束都要执行该匿名办法    defer func() {        //进行异样捕捉        if err := recover(); err != nil {            fmt.Printf("Runtime panic caught: %v\n", err)        }    }()    var i = 1    var j = 0    k := i / j    fmt.Printf("%d / %d = %d\n", i, j, k)}func main() {    divide()    fmt.Println("divide 办法调用结束,回到 main 函数")}

能够看到,尽管会出现异常,但咱们应用recover()捕捉之后,就不会呈现零碎解体退出的情景,而只是将该办法完结。其中fmt.Printf("%d / %d = %d\n", i, j, k)语句并没有执行到,因为代码执行到他的上一步曾经出现异常导致该办法提前结束。
4 recover

当有能够预感的谬误时,又不心愿程序解体退出,能够应用recover()语句来捕捉未解决的panic。recover该当放在defer语句中,且该语句应该在办法中前部,防止未能执行到defer语句时就引发了零碎异样退出。

package mainimport (    "fmt")func divide() {    //通过defer,确保该办法只有执行结束都要执行该匿名办法    defer func() {        //进行异样捕捉        if err := recover(); err != nil {            fmt.Printf("Runtime panic caught: %v\n", err)        }    }()    var i = 1    var j = 0    k := i / j    fmt.Printf("%d / %d = %d\n", i, j, k)}func main() {    divide()    fmt.Println("divide 办法调用结束,回到 main 函数")}

能够看到,尽管会出现异常,但咱们应用recover()捕捉之后,就不会呈现零碎解体退出的情景,而只是将该办法完结。其中fmt.Printf("%d / %d = %d\n", i, j, k)语句并没有执行到,因为代码执行到他的上一步曾经出现异常导致该办法提前结束。

五、总结

通过以上的学习,大家能够以应用为目标的初步理解到go的根底语法,然而仅凭本文想要学明确go是齐全不够的。例如go的最大劣势之一“协程”,因为文章目标就没有特地具体开展,有趣味的同学能够持续学习。