Go语言入门教程

  本教程主要讲解Go语言的面向对象基础和其并发编程,适合有Java等语言编程基础者浏览。Go语言是由Google创建,用来解决类似Google规模级别(Google-scale)的问题。

Go语言有如下特点:

  • 静态类型,  
  • C语言家庭的一部分,  
  • 垃圾收集,   
  • 静态编译,   
  • 面向对象的,   
  • 并发友好。

首先,下载安装Go,一旦安装成功,需要设置一下GOPATH。

$ echo 'export GOPATH=$HOME' >> $HOME/.profile
$ source $HOME/.profile
$ go env | grep GOPATH
GOPATH="/Users/peter"

创建一个main.go文件,如下:

package main

func main() {
 println("hello!")
}

Go语言是一个静态编译型类型语言,因此需要编译后运行:

$ go build
$ ./hello
hello!

创建一个Web服务器

下面我们使用Go创建一个Web服务器,如下代码:

package main

import "net/http"

func main() {

    http.HandleFunc("/", hello)

    http.ListenAndServe(":8080", nil)

}

 

func hello(w http.ResponseWriter, r *http.Request) {

    w.Write([]byte("hello!"))

}

如果熟悉Java等语言,前面两行应该比较熟悉,导入了net/http包。

http.HandleFunc("/", hello)是创建一个http的路由,URL是根路径,然后监听在8080端口。每次针对HTTP服务器根路径的一个新的请求产生时,服务器将生成一个新的协程goroutine执行hello函数。而hello函数简单地使用 http.ResponseWriter将响应写给客户端。该响应是"hello!"我们进行了字节转换。

编译运行后,通过浏览器或curl访问:

$ curl http://localhost:8080
hello!

协程goroutine是Go语言并发编程中的轻量线程概念,并不是真正操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程。

下面看看Go语言的面向对象和并发特性。

面向对象

Go是面向对象的,但没有如下概念:

  • class类
  • extends继承
  • implements实现

我们首先看看Go使用type来表达类型,type关键词定义了一个新的类型,声明作为一个struct,在struct中每个字段可以有一个名称(如下面的Name 和Main),或另外一个struct:

type weatherData struct {

    Name string `json:"name"`

 

}

所有类型都是平等地创建,定义类型的方法是如下定义,不像Java中在Class中定义:

type Door struct {

    opened bool

}

 

func (d *Door) Open() {

    d.opened = true

}

 

func (d *Door) Close() {

    d.opened = false

}

这是Door结构类型中有open()和close()两个方法。类似Java的setter/getter。

 

对于初始类型也可以这样创建:

type Door bool

 

func (d *Door) Open() {

    *d = true

}

 

func (d *Door) Close() {

    *d = false

}

这里是将初始类型bool赋值为true或fals的两个方法。

接口是Go语言的重要特点,其重要性超过协程,它和Java接口有些类似,如下:

In Java:

interface Switch {

    void open();

    void close();

}

In Go:

type OpenCloser interface {

    Open()

    Close()

}

与Java的接口不同的是,Go的接口是不需要显式声明继承的,它是隐式通过编译器根据方法签名匹配的,比如,在Java中实现接口Switch需要编写代码:

public class SwitchImpl implements Switch{ //实现具体内容}

而在Go中接口是隐式通过编译器实现的,前面type Door bool我们已经定义了Door这个类型有两个方法Open和Close,而这里有一个接口type OpenCloser interface申明了两个方法也是Open和Close,编译器也就认为Door implements OpenCloser了。

Go的接口与实现关系是一种隐式满足implicit satisfaction,如果一个类型type定义实现了一个接口的所有方法,那么就认为这个type满足了接口, 隐式满足Implicit satisfaction ==无显式 "implements"代码。它是一种duck typing的Structural subtyping,好处是:

  • 更少依赖
  • 不会造成纷繁类型继承层次
  • 天然的组合特性 非继承

从Go的接口我们看出Go是注重组合超过继承。

组合重于继承

SOLID面向对象设计原则我们已经谈过通过组合实现好于继承,这里可以用一个案例再次证明一下:假设有一个Java线程类:

class Runner {

        private String name;

 

        public Runner(String name) {

            this.name = name;

        }

 

        public String getName() {

            return this.name;

        }

 

        public void run(Task task) {

            task.run();

        }

 

        public void runAll(Task[] tasks) {

            for (Task task : tasks) {

                run(task);

            }

        }

    }

RunCounter继承Runner:

class RunCounter extends Runner {

        private int count;

 

        public RunCounter(String message) {

            super(message);

            this.count = 0;

        }

 

        @Override public void run(Task task) {

            count++;

            super.run(task);

        }

 

        @Override public void runAll(Task[] tasks) {

            count += tasks.length;

            super.runAll(tasks);

        }

 

        public int getCount() {

            return count;

        }

    }

通过如下代码调用:

RunCounter runner = new RunCounter("my runner");

Task[] tasks = { new Task("one"), new Task("two"), new Task("three")};
runner.runAll(tasks);

System.out.printf("%s ran %d tasks\n", runner.getName(), runner.getCount());

运行结果是:

running one
running two
running three
my runner ran 6 tasks

竟然有6个线程任务在跑。而我们是想指定三个啊。这是因为继承导致了弱封装,封装性不强,产生了紧耦合,导致不可思议的Bug:

解决方案是组合Composition:

class RunCounter {

        private Runner runner;

        private int count;

 

        public RunCounter(String message) {

            this.runner = new Runner(message);

            this.count = 0;

        }

 

        public void run(Task task) {

            count++;

            runner.run(task);

        }

 

        public void runAll(Task[] tasks) {

            count += tasks.length;

            runner.runAll(tasks);

        }

 

        public int getCount() {

            return count;

        }

 

        public String getName() {

            return runner.getName();

        }

    }

虽然解决了问题,但是缺点是需要在RunCounter的显式定义Runner方法:

public String getName() { return runner.getName(); }

导致很多重复,也会引入Bug。

在Go中没有继承,天然是组合,直接实现如下:

type Runner struct{ name string }

 

func (r *Runner) Name() string { return r.name }

 

func (r *Runner) Run(t Task) {

    t.Run()

}

 

func (r *Runner) RunAll(ts []Task) {

    for _, t := range ts {

        r.Run(t)

    }

}

RunCounter的实现如下:

type RunCounter struct {

    runner Runner

    count  int

}

 

func NewRunCounter(name string) *RunCounter {

    return &RunCounter{runner: Runner{name}}

}

 

func (r *RunCounter) Run(t Task) {

    r.count++

    r.runner.Run(t)

}

 

func (r *RunCounter) RunAll(ts []Task) {

    r.count += len(ts)

    r.runner.RunAll(ts)

}

 

func (r *RunCounter) Count() int { return r.count }

 

func (r *RunCounter) Name() string { return r.runner.Name() }

虽然这里也有Name()这个方法,但是我们可以去除它,首先来看看Go语言的Struct embedding,也就是struct嵌入。被嵌入的类型的方法和字段在嵌入者类型中定义实现。虽然类似继承,但是被嵌入者不知道它被嵌入了。例如一个类型Person:

type Person struct{ Name string }

func (p Person) Introduce() { fmt.Println("Hi, I'm", p.Name) }

我们能定义Employee 嵌入了Person:

type Employee struct {

    Person

    EmployeeID int

}

这样所有的Person字段方法都适用Employee:

var e Employee
e.Name = "Peter"
e.EmployeeID = 1234

e.Introduce()

现在我们使用struct嵌入来优化前面的RunCounter:

type RunCounter2 struct {

    Runner

    count  int

}

 

func NewRunCounter2(name string) *RunCounter2 {

    return &RunCounter2{Runner{name}, 0}

}

 

func (r *RunCounter2) Run(t Task) {

    r.count++

    r.Runner.Run(t)

}

 

func (r *RunCounter2) RunAll(ts []Task) {

    r.count += len(ts)

    r.Runner.RunAll(ts)

}

 

func (r *RunCounter2) Count() int { return r.count }

嵌入是不是像继承呢?但是它不是,而是更好 ,它是组成组合Compistion。你不能进入另一种类型改变它的工作方式。它的调度方法是显式明确的。

从某种角度上说,struct嵌入类似依赖注入(DI)或反转模式,通过组合+依赖注入替代了以前的继承。

下面看看接口的Struct embedding嵌入:

如果一个T类型被嵌入到类型E中的字段,E的所有方法将在T类型中定义,这样,E如果是接口,T必须隐式满足E。也就是说,T必须实现接口E。

我们定义loopBack类型,net.Conn类型被嵌入了类型loopBack中,而net.Conn是一个接口:

type loopBack struct {

    net.Conn

    buf bytes.Buffer

}

任何调用net.Conn的方法都会出错,因为这个字段是一个空的nil. 下面我们定义其操作:

func (c *loopBack) Read(b []byte) (int, error) {

    return c.buf.Read(b)

}

 

func (c *loopBack) Write(b []byte) (int, error) {

    return c.buf.Write(b)

}

那么可以认为loopBack是接口net.Conn的实现。

 

并发性

并发的特点是需要锁Lock和互斥Mutex。在Java中加锁和解锁是一个复杂过程代码如下:

try {

  mutex.acquire();

  try {

    // do something

  } finally {

    mutex.release();

  }

} catch(InterruptedException ie) {

  // ...

}

并发另外一个特性是异步,各种语言都有自己的异步机制,基于回调的有:

  1. Ruby的 EventMachine
  2. Python的 Twisted
  3. NodeJS

但是不能很好地与并行共处,依赖各种库包,代码难于调试,易陷入回调嵌套地狱。见callbackhell.com

Go的并发基于两个概念:

  • 协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程。
  • 通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。

比如sleep和taalk代码如下:

func sleepAndTalk(t time.Duration, msg string) {

    time.Sleep(t)

    fmt.Printf("%v ", msg)

}

每秒一个消息:

func main() {

    sleepAndTalk(0*time.Second, "Hello")

    sleepAndTalk(1*time.Second, "Gophers!")

    sleepAndTalk(2*time.Second, "What's")

    sleepAndTalk(3*time.Second, "up?")

}

如果我们不是每秒,而是需要同时发送消息呢?加上go即可:

func main() {

    go sleepAndTalk(0*time.Second, "Hello")

    go sleepAndTalk(1*time.Second, "Gophers!")

    go sleepAndTalk(2*time.Second, "What's")

    go sleepAndTalk(3*time.Second, "up?")

}

这是main开启一个主协程,当其结束整个程序也就结束。

下面我们看看通过Channel进行通讯,这时sleepAndTalk 就不是打印出信息,而是将字符串发送给channel了。

func sleepAndTalk(secs time.Duration, msg string, c chan string) {

    time.Sleep(secs * time.Second)

    c <- msg

}

我们创建channel然后将其传递给sleepAndTalk, 之后就可以等待数据值发送到channel了:

func main() {

    c := make(chan string)

 

    go sleepAndTalk(0, "Hello", c)

    go sleepAndTalk(1, "Gophers!", c)

    go sleepAndTalk(2, "What's", c)

    go sleepAndTalk(3, "up?", c)

 

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

        fmt.Printf("%v ", <-c)

    }

}

下面看看如何在Web环境中实现:首先我们从Channel中接受到nextId:

var nextID = make(chan int)

 

func handler(w http.ResponseWriter, q *http.Request) {

    fmt.Fprintf(w, "<h1>You got %v<h1>", <-nextID)

}

需要一个协程发送nextID到channel中。

func main() {

    http.HandleFunc("/next", handler)

    go func() {

        for i := 0; ; i++ {

            nextID <- i

        }

    }()

    http.ListenAndServe("localhost:8080", nil)

}

通过浏览器访问localhost:8080/next 可得到nextID数值。

如果有多个Channel,那么代码如下:

var battle = make(chan string)

 

func handler(w http.ResponseWriter, q *http.Request) {

    select {

    case battle <- q.FormValue("usr"):

        fmt.Fprintf(w, "You won!")

    case won := <-battle:

        fmt.Fprintf(w, "You lost, %v is better than you", won)

    }

}

这样访问的URL参数不同:

Go - localhost:8080/fight?usr=go
Java - localhost:8080/fight?usr=java

多个Channel可以串联组成流:

gophers链:

func f(left, right chan int) {

    left <- 1 + <-right

}

 

func main() {

    start := time.Now()

    const n = 1000

    leftmost := make(chan int)

 

    right := leftmost

    left := leftmost

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

        right = make(chan int)

        go f(left, right)

        left = right

    }

 

    go func(c chan int) { c <- 0 }(right)

 

    fmt.Println(<-leftmost, time.Since(start))

}

 

参考:

Go语言Goroutine与Channel内存模型

Go语言是彻底的面向组合的并发语言

Go入门multiWeatherProvider代码案例

学习GO语言PDF (来自开源学习GO Github

Go代码在线调试器

你不能在JVM上实现Actor, 绿色线程和CSP

Actor模型和CSP模型的区别

Go语言专题

Docker专题