---
layout: mypost_no_toc
title: 开始go语言学习
categories: [tech]
toc: true
---
在公司搬了 5 年砖了,一直用着 C++ 作为主要的开发语言。期间也写过一些 Java,夹杂着一些 Python 用来写脚本,还有大量的 protobuf(PS:写序列化格式也算语言?emmm,不过定义好 API 和数据结构,在搬砖的过程中确实已经完成了一半工作量了)。极少数的时候需要用到 Go 来修改一些用 Go 写的工具。
## 为什么要学 Go
作为一个程序员,说实话,我并不是一个对各种语言有"追求"的人。什么语言好用就用什么,能完成工作就行。但是断断续续接触了不少 Go 语言的特性以后,这个**语法上更为现代,并且性能上在各个方面都有不错表现的语言**,让我决定可以试着深入了解一下。
虽然我在大多数时候还是会怀疑 Go 在安全性的考虑上会在性能上做一些妥协。不过安全性这点比起 C/C++ 来说,完全就是**开一辆有着 ABS、车内电脑控制的超跑和全手动的 F1 之间的区别**。
所以在项目中,为了安全而选择牺牲一点性能是完全值得的。尤其是和 Java 比较的时候,能够拥有类似的安全性但是更高的性能,这点太香了。
### 真正让我下定决心的项目
对于 Go 的大部分深入认识,是在接触了 **trojan-go** 这个项目之后。我阅读了 trojan 和 trojan-go 两个项目的代码,一个是 C++ 写的,一个是 Go 写的。
比较的结果很明显:**像这类需要在网络层使用并发的程序,使用 Go 会带来很多方便。**
goroutines 和 channel 这些极为方便又不损失多少性能的抽象,简直是太好用了。在 C++ 里要实现类似的功能,得自己管理线程池、锁、条件变量这一堆东西,写起来又累又容易出错。而 Go 里面,几行代码就搞定了。
所以,现在我从一个半吊子 C++ 程序员的角度出发,从 Go 的基础特性开始学习使用 Go 语言。
# Golang 的一些基础特性
从一个 C++ 程序员的角度来看,Go 有很多让我眼前一亮的特性,也有一些让我觉得有点别扭的地方。下面记录一下我的学习过程和感受。
## 类型推断
从字面值或者返回值,不需要明确指出类型,编译器也知道这个是什么类型。就像 C++11 之后的 `auto` 关键词:
```go
var x = 1.0 // x 是 float64
var y = getObjectY()
这点很好,从 C++ 切换过来的时候可以很快地适应。不用每次都写一大串类型名,尤其是那些模板类型,写起来真的很烦。
Go 是有指针类型的,并且操作也和 C++ 一样,使用的是 & 和 *。
var s Something = getSomething();
var sp *Something = &s;
var ss Something = *sp; // 从 sp 复制并新建一个 Something
也可以用 new 关键词来创建一个新指针:
var p *Something = new(Something)
这点上和 C++ 非常类似。赋值会进行复制操作,不像 Java,非基本类型都是通过引用传递的。
有一点不同的是,在 Go 里面没有 ->,也没有指针运算。无所谓指针类型和普通类型,成员函数的访问都是通过 . 进行操作:
var a *Something = new(Something)
var b Something = *a
a.do();
b.do();
这点我很喜欢,因为编译器永远都会知道值的类型,所以无所谓到底用的是 . 还是 ->。在 C++ 里有时候看到一个 . 或者 ->,还得想一想这个变量到底是指针还是值。
没有指针运算,意味着没法像 C++ 一样对指针进行 ++ 操作来遍历数组。必须通过索引来访问数组元素。这个涉及到了边界检查,就像一开始说的,这点可以有效地防止越界而造成的安全漏洞。虽然有一点性能损失,但是安全性提升了很多,值得。
Go 函数可以通过值或指针获取参数,这类似于 C++,但与 Java 不同(Java 总是通过引用获取非基本类型)。
与 Java 一样,Go 没有常量指针或常量引用的概念。因此,如果函数将参数作为指针,编译器无法阻止我们更改它指向的值。在 Java 中,这通常是通过创建一个不可变类型来完成的,而许多 Java 类型,例如 String,都是不可变的。
但我更喜欢 C++ 中的常量性、指针/引用参数和在运行时初始化的值的语言支持。 Go 在这方面确实有点遗憾。
Go 没有类型之间的隐式转换,比如 int 到 uint,或 float 到 int。这也适用于通过 == 和 != 进行的比较。
所以,这就不会编译:
var x int = -1
var y uint = x // 报错
var a = 1.0
var b int = a // 报错
C++ 编译器警告可以捕获其中的一些,语言检查器也可以做到这点。在工程应用上,这些隐式转换是需要被避免的,所以 Go 直接在语言层面禁止了,我觉得挺好的。
Go 仍然具有无符号整数。与 C++ 的标准库不同,Go 使用有符号整数表示大小和长度。这点在我们公司内部已经使用 int64、int32 之类的 alias 来替换标准库里的 long 和 int 了。希望 C++ 标准库有一天也能做到这一点。
Go 的限制很严格,甚至定义的类型之间的隐式转换也是不被允许的。所以,这个例子就不会编译:
type AliasI int
type AliasJ int
var a AliasI = 100
var b AliasJ = a // 出错
而在 C++ 中,这只是 typedef,完全可以互相赋值。所以这点上,我希望在使用 type 定义变量时,将其视为 C++ 编译器中的警告而不是无法编译。有时候这个限制有点过于严格了。
不过,我们可以将基础类型的(无类型)值隐式分配给类型化的变量,但不能将基础变量隐式分配给类型化的变量:
type AliasI int
var a AliasI = 100 // 可以编译
var i int = 100
var b AliasI = i // 无法编译
c++里面引用和值以及指针都是可以很方便地使用。Go 看上去也有引用(emm,看上去像值的指针?),但是只适用内置的 slice、map 和 channel 类型。例如,函数可以更改其输入slice参数,并且该更改将对调用者可见,即使该参数未声明为指针:
func doThing(someSlice []int) {
someSlice[2] = 3;
}
类似的在 C++ 中,很明显可以看出这是一个引用:
void doThing(Thing& someSlice) {
someSlice[2] = 3;
}
我不确定这是否是Go的基本特征,或者只是关于这些特定类型是如何实现的。只有不同的类型会不一样(类似java函数参数的传值和传引用),这点令我在刚开始接触的时候会感觉到困惑。一个语言能做到便利性很好,但是在一致性上也需要考虑好。
Go 的 const 关键字不像 C++ 中的 const ,它表示变量的值在初始化后不应更改。它更像是 C++11后的constexpr关键字。编译时确定值,具有类型安全性。例如:
const pi = 3.1415
请注意,我们没有为 const 值指定类型,因此根据值的语法,该值可以用于各种类型,有点像 C 宏 #define。但是我们可以通过指定类型来限制它:
const pi float64 = 3.1415
不过与 C++ 中的 constexpr 不同,没有可以在编译时计算constexpr函数,因此不能这样做:
const pi = calculate_pi()
type Point struct {
X int
Y int
}
const point = Point{1, 2}
虽然你可以用一个简单的类型来做到这一点,它的基础类型可以是 const:
type Yards int
const length Yards = 100
Go 中的所有循环都是for循环,没有 while 或 do-while 循环。与 C、C++ 或 Java 相比这简化了语言。例如:
for i := 0; i < 100; i++ {
...
}
或者,就像 C 中的 while 循环:
for keepGoing {
...
}
并且 for 循环对诸如字符串、切片或映射之类的容器具有基于范围的语法:
for i, c := range things {
...
}
for _, c := range things {
...
}
C++11 以后也有基于范围的 for 循环。但我喜欢 Go 的for,可以同时给你索引和值。
Go 有一个内置的字符串类型,以及内置的比较运算符,例如 ==、!= 和 <(Java 也是如此)。与 Java 一样,字符串是不可变的,因此在创建它们之后您无法更改它们,尽管您可以通过使用内置运算符 + 连接其他字符串来创建新字符串。例如:
str1 := "foo"
str2 := str1 + "bar"
Go代码是UTF-8编码,字符串文字可能包含非ASCII的utf-8字符。Go将Unicode字符称为“runes”。内置的len()函数返回的是字节数,用于字符串的运算符[]也是对字节进行操作。不过有一个utf8函数是将字符串以“runes”处理:
str := "foo"
l := utf8.RuneCountInString(str)
// 基于范围的 for 循环处理,而不是字节:
str := "foo"
for _, r := range str {
fmt.Println("rune: %q", r)
}
C++ 仍然没有标准的等价物。
Go 的切片有点像 C 中的动态分配数组,尽管它们实际上底层还是数组,只是在此基础上提供了一个类似于view的抽象。两个切片也可以是同一个底层数组不同部分的view。它们感觉有点像 C++17 中的 std::string_view 或 GSL::span,但它们可以轻松调整大小,类似 C++ 中的 std::vector 或 Java 中的 ArrayList。
我们可以像这样声明一个跨度,并附加到它:
a := []int{5, 4, 3, 2, 1} // A slice
a = append(a, 0)
数组(与切片不同,其大小不能改变)具有非常相似的语法:
a := [...]int{5, 4, 3, 2, 1} // An array.
b := [5]int{5, 4, 3, 2, 1} // Another array.
您必须小心通过指针将数组传递给函数,否则它们将被(深度)按值复制。
切片不是(深度)可比较或可复制的,这与 C++ 中的 std::array 或 std::vector 不同,这点上感觉相当不方便。
如果调用内置的append()函数发现需要的容量超过现有容量(可能超过当前长度),则它可能会分配更大的底层数组。
在切片里面,你不能保留指向切片元素的指针。如果可以,垃圾收集系统将需要保留先前的底层数组,直到停止使用该指针为止。
与C++ 数组不同,也与std::vector的[]不同,尝试访问切片的无效索引将导致panic,而不仅仅是未定义的行为。这个更为安全,虽然边界检查有一些小的性能成本。
Go 有一个内置的map类型。这大致相当于 C++ std::unordered_map(哈希表)。不知道是链式哈希表(std::unordered_map)还是开放寻址的哈希表(C++标准库里面没有)。在map中,key是需要可以被比较和计算hash的。所以只有基本类型(int、float64、string 等,但不是切片)或仅由基本类型组成的结构是可比较的,从而用来做key。这点上 C++ 更自由,只需要重载std::hash
与 C++ 不同,Go不能在map中存指向元素的指针,因此更改值的一部分就意味着将整个值复制并替换。Go这样做是为了避免在map扩表时出现无效指针的问题。
map用起来像这样:
m := make(map[int]string)
m[3] = "three"
m[4] = "four"
Go 中的函数可以有多个返回类型,我发现这比c++,Java输出参数更加显式。例如:
func getNFoo() (int, Foo) {
return 2, getFoo()
}
a, b := getNFoo()
这有点像在C++11的返回tuple,尤其是在C++17后使用结构化绑定:
std::tuple<int, Foo> get_n_foo() {
return make_tuple(2, get_foo());
}
auto [a, b] = get_things();
与 Java 一样,Go 具有自动内存管理功能。所以可以愉快地这样做,而不必担心需要去手动释放内存:
func getThing() *Thing {
a := new(Thing)
...
return a
}
b := getThing()
b.foo()
甚至可以这样做,不用关心,甚至不需要知道实例是在堆栈还是堆上创建的:
func getThing() *Thing {
var a Thing
...
return &a
}
b := getThing()
b.foo()
这题用到了动态创建slice,使用make语法来创建一个
func grayCode(n int) []int {
length := 1 << n
nums := make([]int, length)
for i := 1; i < length; i++ {
nums[i] = i ^ (i >> 1)
}
return nums
}
这题主要就是数组操作,go里面的二维数组相当于是一个在创建的时候需要对每一行进行声明,这点没有c++方便。 go的二维数组在声明和初始化的时候缺少必要的标准库支持,这是我一开始没有预料到的,缺少了一点方便。有点类似于Java的 List<List<», 每一次往里添加的时候得new一个新的List出来。不过这并不能算是太大的问题。
func matrixReshape(mat [][]int, r int, c int) [][]int {
if r * c != len(mat) * len(mat[0]) {
return mat
}
result := make([][]int, r)
i := -1
j := c
for _,row := range mat {
for _,value := range row {
if j == c {
j = 0
i++
result[i] = make([]int, c)
}
result[i][j] = value;
j++
}
}
return result;
}
这题用了map,slice以及使用了sort包。 sort包里主要就是为了操作数组进行排序工作。在题目里需要将数据按逆序排序。
import (
"sort"
)
func minSetSize(arr []int) int {
m := make(map[int]int)
for _,v := range arr {
m[v]++
}
l := make([]int, 0, len(m))
for _, f := range m {
l = append(l, f)
}
// sort.Sort(sort.Reverse(sort.IntSlice(l))) //
sort.Slice(l, func(i, j int) bool { return l[i] > l[j] })
count := 0
for i,v := range l {
count += v
if (count >= len(arr)/2) {
return i+1;
}
}
return 0; // Never happen
}