部门计划年底开始转到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-tour和go 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中的实现是一致的。
映射
代码如下:
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()
}
}