本教程主要讲解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) {
// ...
}
并发另外一个特性是异步,各种语言都有自己的异步机制,基于回调的有:
- Ruby的 EventMachine
- Python的 Twisted
- 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语言PDF (来自开源学习GO Github)