部门计划年底开始转到go,难得这个周末在家没事,学习go圣经这里记录下与C C++ lua中的异同点。对比主要涉及ANSI C 、C++98、 C++11(小部分)、 lua,不涉及c++14 c++17 c++20等新特性。在学习go的过程中一个下意识的举动就是想某某特性C/C++ lua里对应怎么实现?
学习的直观感受是go中有的特性,C/C++ lua都有,但大部分情况下没有go用起来简单、方便。语法和使用上C/C++远比go复杂的多,go入门快,学习性价比高。初学go,文章中的观点如有错误,欢迎评论指出。本文部分例子来源于go-tourgo by example这两个教程非常好,非常适合快速入门了解go语言。

代码风格

函数的左括号{必须和func函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行。

函数的左括号{必须和函数声明在同一行,这一点不是规范而是硬性规定,有点霸道!不过这么做的好处是代码风格能比较统一,不像C/C++ 中一样,怎么写都行,换几个行都没有问题。go原生提供了gofmt 格式化工具,C/C++有第三方代码格式化工具,在实际开发中基本不使用这些代码格式化工具而是依赖编程经验checklist予以规范。

// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/

// See page 4.
//!+

// Echo1 prints its command-line arguments.
package main

import (
    "fmt"
)

func main() {
    var x ,y = 10,20
    var sum = x+
    y
    fmt.Println(sum)
}

//!-

go返回函数中局部变量的地址安全

在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量,这里有点不可思议,推测编译器这里有优化,函数f中的变量v是在堆中申请的。

var p = f()

func f() *int {
    v := 1
    return &v
}

参考如下C演示代码:

#include <stdio.h>

int* f()
{
    int a = 10 ;
    return &a;
}

int main()
{
    int *p = f();
    if(NULL == p)
    {
        printf("pointer p empty.\n");
    }
    return 0;
}

编译运行:

[root c++]#gcc -g -Wall func.c
func.c: In function ‘f’:
func.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]
     return &a;
            ^~
[root c++]#
[root c++]#
[root c++]#./a.out
pointer p empty.

这里可以看到编译时有告警,指出函数返回的是一个局部变量的地址。应当是编译器进行了优化,判断返回的是局部变量的地址然后返回了NULL,如果读写指针所对应的地址会导致segement fault,以便尽早发现该问题。

package概念

go中的package(包)有点类似python java等语言,对于C语言的话有点像so库,目的都是为了模块化,多人协作开发和代码重用。在go语言中,一个简单的规则时:如果一个名字是大写字母开头的,那么该名字是导出的。C语言中通过gcc扩展,在代码中可以指定so中哪些函数对外导出可用。
go中另一点比较好的是,如果导入了包却没有使用会被当作编译错误处理。可以减少不必要的包导入,提高性能,代码尽可能简洁。当然如果有特例可以在导入前面加_。
特别是针对包的初始化有init初始化函数,跟python非常的相似。例子:

package main

import (
    "log"
    "os"

    _ "github.com/goinaction/code/chapter2/sample/matchers"
    "github.com/goinaction/code/chapter2/sample/search"
)

go中flag包

阅读2.3变量时有提到指针是实现标准库中flag包的关键技术,flag包的作用看了下例子,这不就是解析命令行参数吗?
C里面同样有get_opt系列函数用于解析命令行参数,用法和go中的flag包基本一致,C命令行参数解析.

元组赋值

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

x, y = y, x
a[i], a[j] = a[j], a[i]

C语言里对这个变量交换特别亲切,一般是中间变量tmp

int temp;
temp = a;
a = b;
b = temp;

后面遇到过一些所谓的"面试题",如果不用temp交换两个变量的值:
下面常见的两种方法实际是一致的:

//method 1
a=a+b;
b=a-b;
a=a-b;

//method 2
a=a^b;
b=a^b;
a=a^b;

看到go中的交换两个变量的形式,更加清晰,简练。这种写法赋值语句右边的值先计算,实际上就是y赋值给x,x赋值给y,这里x还是第一步赋值之前的x。
go中的例子:

func gcd(x, y int) int {
    for y != 0 {
        x, y = y, x%y
    }
    return x
}

实际上这是辗转相除法,对应的C实现:

int gcd(int x,int y)
{
    while(b)
    {
        /*利用辗除法,直到b为0为止*/
        int temp = y;
        y = x % y;
        x = temp;
    }
    return x;
}

实际上go可以改写成:

func gcd(x, y int) int {
    for y != 0 {
        //x, y = y, x%y

    var temp = y;
    y = x % y;
    x = temp;
    }

    return x
}

把x保存到temp和y保存到temp实际是一致的,这里为了和上面的C代码对应,帮助理解go中的这种元组赋值。

返回多个值

go中函数可以有多个返回值,例如os.Open函数

f, err = os.Open("foo.txt") // function call returns two values

lua里也有类似的机制,比如下面这个函数,返回最大值和最大值对应的下标:

function maximum (a)
    local mi = 1             -- 最大值索引
    local m = a[mi]          -- 最大值
    for i,val in ipairs(a) do
       if val > m then
           mi = i
           m = val
       end
    end
    return m, mi
end

print(maximum({8,10,23,12,5}))

C语言中要返回多个值,只能通过参数传递指针 引用等将返回结果带回,return value必须是一个,当然可以将需要返回的数据封装好。
C/C++里可以学习geeksforgeeks中的文章。How to return multiple values from a function in C or C++?

type

go里使用type创建一个新的类型名称,和已有类型具有相同的底层结构。有意思的是,即使底层类型相同,新类型也不兼容,这里跟C/C++里使用typedef using(C++11)声明的新类型是不同的。
go中示例代码

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

Celsius和Fahrenheit分别对应不同的温度单位。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。

作用域

学习gopl2.7章节时发现go中作用域和生命周期居然是两个概念。
C里的生命周期(作用域)概念相对简单,可以简单理解为作用域就是在包裹变量的最内层花括号中。C语言和go一致的是:局部变量会覆盖掉全局变量,go里面是局部变量覆盖包级变量。
go里面的生命周期和C++ 11之后引入右值引用之后比较像,C++在用临时对象或函数返回值给左值对象赋值时的深度拷贝(deep copy)一直受到诟病。考虑到临时对象的生命期仅在表达式中持续,如果把临时对象的内容直接移动(move)给被赋值的左值对象,效率改善将是显著的。这一点应当是和go中的实现是一致的。

映射

go指南-映射
代码如下:

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])
    //不存在的映射打印类型零值
    fmt.Println(m["not exist"])
}

这里make函数返回给定类型的映射,并将其初始化备用
go指南-映射的文法,这里的代码如下:

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": Vertex{
        40.68433, -74.39967,
    },
    "Google": Vertex{
        37.42202, -122.08408,
    },
}

func main() {
    m["hello"] = Vertex{
    20.368,30.256,
}
    fmt.Println(m)
}

这里我在main函数中同样可以添加键值对,那么前面使用make函数返回给定类型的映射,有什么区别呢?
难道仅仅是使用var m = map[string]int时,必须指定初值?

鸭子类型

鸭子类型-维基百科
几年前还在读书的时候某个间歇性发奋的早上看C++教程时有学习到鸭子类型,当时迷迷糊糊,直到看到go种的例子,有种恍然大悟的感觉。
C++的多态主要通过继承来实现,一个耳熟能详的例子:
形状类拥有draw方法,下面的定义意味着所有的子类必须实现draw函数,所以可以认为shape是定义了一个接口(类似java)。

class shape {
public:
  …
  void draw(const position&) = 0;
};

另一种实现多态的例子是:鸭子类型
就这几天的学习发现,go中没有继承,组合优于继承在go里得到了充分的展现。

如果一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那么这只鸟就可以被当作鸭子。

在go中不需要继承shape来实现circle triangle等类,可以直接定义一个接口,接口是用来定义行为的类型。接口类型是由一组方法签名定义的集合。接口类型的变量可以保存任何实现了这些方法的值。
来自接口与隐式声明的一个例子可以说明这一点:

package main

import "fmt"

type Shape interface {
    Draw()
}

type Circle struct {
    S string
}

type Triangle float64

// 此方法表示类型 Circle 实现了接口 Draw,但我们无需显式声明此事。
func (t Circle) Draw() {
    fmt.Println(t.S)
}

// 此方法表示类型 Triangle实现了接口Draw ,但我们无需显式声明此事
func (t Triangle) Draw() {
    fmt.Println(t)
}

func main() {
    var i Shape = Circle{"hello"}
    i.Draw()
    
    var f Shape = Triangle(3.1415926)
    f.Draw()
}

并发

看go语言中的并发印象深刻的一点是上手快,不需要特别多的知识就能写出漂亮的代码。经典著作UNIX网络编程卷2进程间通信(UNP2)一书中的介绍了进程间通信IPC(interprocess communication)。目前在工作中用到的跨平台IPC接口是公司大佬们若干年前所封装,使用非常简单,屏蔽了底层细节,特别是用在跨平台编程,运行这个服务的两个进程(可以在不同机器 平台(linux windows))中相互通信,例如linux机器给windows机器发送消息请求,在回调中处理windows端发回的数据。
UNP2这本书里介绍了4种不同的IPC形式:
(1)管道(go 中chan) FIFO 和消息队列
(2)同步(互斥锁 条件变量 读写锁 文件锁 记录锁 信号量) (go中 互斥锁)
(3) 共享内存
(4)远程过程调用
一套组合拳下来,上手程度比go难多了,以一个简单的例子说明go中的并发:

// This sample program demonstrates how to use the atomic
// package to provide safe access to numeric types.
package main

import (
    "fmt"
    "runtime"
    "sync"
    "sync/atomic"
)

var (
    // counter is a variable incremented by all goroutines.
    counter int64

    // wg is used to wait for the program to finish.
    wg sync.WaitGroup
)

// main is the entry point for all Go programs.
func main() {
    // Add a count of two, one for each goroutine.
    wg.Add(2)

    // Create two goroutines.
    go incCounter(1)
    go incCounter(2)

    // Wait for the goroutines to finish.
    wg.Wait()

    // Display the final value.
    fmt.Println("Final Counter:", counter)
}

// incCounter increments the package level counter variable.
func incCounter(id int) {
    // Schedule the call to Done to tell main we are done.
    defer wg.Done()

    for count := 0; count < 2; count++ {
        // Safely Add One To Counter.
        atomic.AddInt64(&counter, 1)

        // Yield the thread and be placed back in queue.
        runtime.Gosched()
    }
}