go语言设计中几个有意思的地方。

作者 Lu Liang 日期 2018-10-01
go
go语言设计中几个有意思的地方。

go语言的成功是典型实用主义思维的成功。go语言就像是一个面向过程(C),面向对象(JAVA)及函数编程(javascript)的揉合体,充分消化吸取这些编程思想中的最实用点。在go中,我们可以看到,C的结构体和指针,JAVA的接口,及函数编程中函数是一等公民等等特性。下面我们就来浅析一下这些有意思的地方。

指针的运用。

C/C++中,最容易出问题,最令人诟病的就是指针。当初在JAVA的设计中为了解决这个问题,就彻底抛弃了指针。虽然go保留了指针,但是go编译器帮助做了优化, 使用起来更加自然。

  • 编译器帮你进行指针类型的隐式转换

如下例:通过指针employeeOfTheMonth和操作符. 就像Java一样很自然的设置Position

var employeeOfTheMonth *Employee = &dilbert
employeeOfTheMonth.Position += " (proactive team player)"
  • 调用对象方法时,也一样。
pptr.Distance(q) 等价于 (*pptr).Distance(q)
  • 操作指针时,也只是更新内容而不是指针本身。这样就减少了C/C++中使用指针带来的内存问题。
func incr(p *int) int {
*p++ // 改变指针P指向的变量的值,而不是指针P
return *p
}

函数是一等公民

函数作为一等公民可以赋值,也可以作为函数的输入输出值。

func square(n int) int { return n * n }
func negative(n int) int { return -n }
func product(m, n int) int { return m * n }

f := square
fmt.Println(f(3)) // "9"

f = negative
fmt.Println(f(3)) // "-3"
fmt.Printf("%T\n", f) // "func(int) int"

f = product // compile error: can't assign func(int, int) int to func(int) int

结构体Tag。

JSON对象几乎是WEB开发中必不可少的,go通过结构体tag可以很自然的在结构体和JSON对象间进行自如转换。
例如:下面的代码就通过json.Marshal使得Year转换成JSON对象时的released。另外,如果希望输出的格式有缩进,可以使用json.MarshalInden来代替json.Marshal。

// 定义结构体 及 在Year,Color上加Tag
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}

// 初始化 movies
var movies = []Movie{
{Title: "Casablanca", Year: 1942, Color: false,
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
{Title: "Cool Hand Luke", Year: 1967, Color: true,
Actors: []string{"Paul Newman"}},
{Title: "Bullitt", Year: 1968, Color: true,
Actors: []string{"Steve McQueen", "Jacqueline Bisset"}},
// ...
}

// 转换成json
data, err := json.Marshal(movies)
if err != nil {
log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

// 输出结果
[{"Title":"Casablanca","released":1942,"Actors":["Humphrey Bogart","Ingr
id Bergman"]},{"Title":"Cool Hand Luke","released":1967,"color":true,"Ac
tors":["Paul Newman"]},{"Title":"Bullitt","released":1968,"color":true,"
Actors":["Steve McQueen","Jacqueline Bisset"]}]

模版文件使得格式化输出更加丰富

go中支持text/template和html/template等模版文件,下面的代码就是以text/template为例,来看看如何使用template。

  1. 定义模版文件

    const templ = `{{.TotalCount}} issues:
    {{range .Items}}----------------------------------------
    Number: {{.Number}}
    User: {{.User.Login}}
    Title: {{.Title | printf "%.64s"}}
    Age: {{.CreatedAt | daysAgo}} days
    {{end}}`
  2. 解析模版文件,创建模版对象并注册模版文件中要使用的函数

    // 定义函数
    func daysAgo(t time.Time) int {
    return int(time.Since(t).Hours() / 24)
    }

    // call .New .Funcs .Parse
    report, err := template.New("report").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ)
    if err != nil {
    log.Fatal(err)
    }
  3. 调用模版对象,进行输出。

    var report = template.Must(template.New("issuelist").
    Funcs(template.FuncMap{"daysAgo": daysAgo}).
    Parse(templ))

    func main() {
    result, err := github.SearchIssues(os.Args[1:])
    if err != nil {
    log.Fatal(err)
    }

    if err := report.Execute(os.Stdout, result); err != nil {
    log.Fatal(err)
    }
    }

关注于数据结构及其行为。

go和Java等面向对象编程语言相比,在面向对象方面进行了简化。 例如:没有访问级别, 没有继承(通过组合来实现继承);go重点关注于数据结构本事及其行为(这里指方法和实现的接口)。

//通过组合来实现继承的例子

import "image/color"

type Point struct{ X, Y float64 }

type ColoredPoint struct {
Point
Color color.RGBA
}

var cp ColoredPoint
cp.X = 1 // <--- assign 1 to cp.Point.X
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"

defer关键字

defer关键字确保了资源的释放,相当于Java中的finally关键字。

func Balance() int {
mu.Lock()
defer mu.Unlock()
return balance
}

Goroutine和CSP并发模型。

下面是一个使用goroutine和channel的例子:数据经过channel就像pipeline一样被不同的goroutine处理。# counter -> squarter -> printer

func counter(out chan<- int) {
for x := 0; x < 100; x++ {
out <- x
}
close(out)
}

func squarer(out chan<- int, in <-chan int) {
for v := range in {
out <- v * v
}
close(out)
}

func printer(in <-chan int) {
for v := range in {
fmt.Println(v)
}
}

func main() {
naturals := make(chan int)
squares := make(chan int)
go counter(naturals)
go squarer(squares, naturals)
printer(squares)
}

goroutine 是Go语言中并发的执行单位。从OS的角度来看真正工作的是线程,goroutine和线程的关系如下:

一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个上下文连接一个或者多个Goroutine。P(Processor)的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数runtime.GOMAXPROCS()进行设置。Processor数量固定意味着任意时刻只有固定数量的线程在运行go代码。Goroutine中就是我们要执行并发的代码。图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues

三者关系的宏观的图为:

go语言在os用户空间中设计了goroutine,通过dynamice stack(一个goroutine 2k-1G) 及上图的 m:n scheduler成功在多核处理器上实现了大量的并发。

相比较actor模型的并发可以扩展到多个机器上,go是如何支持扩展到多个机器上的呢, channel over rpc, message queue?

附1,其它语言在协程上的支持如下:

附2,Actor和CSP模型:

性能优化

go注重性能优化,测试框架中自带了Benchmark测试和性能profile报告.

import "testing"

func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}

$ go test -cpuprofile=cpu.out
$ go test -blockprofile=block.out
$ go test -memprofile=mem.out