博主之前使用的语言主要是C++,目前想要学习一下Golang,这篇Blog主要是记录一下Golang和C++基础语法的一些区别。
1. Golang 简介
2. Golang 基础语法
2.1. 行分隔符
Golang 使用换行符
作为行分隔符,而C++使用分号作为行分隔符。
- 如果打算将多个语句写在同一行,可以使用分号作为语句之间的分隔符。
- 如果一行的长度超过了编辑器的显示范围,可以使用反斜杠
\
作为换行符。
fmt.Println("Hello, World!")
fmt.Println("Hello, World!"); fmt.Println("Hello, World!")
fmt.Println(" \
Hello, World! \
")
std::cout << "Hello, World!" << std::endl;
2.2. 注释
Golang 和 C++ 都支持单行注释和多行注释,语法也都一样。
2.3. 包
Golang 程序是通过包(package)来组织的,包类似于其它编程语言中的库(library)或模块(module)的概念。一个包可以包含多个文件,一个文件夹下的所有文件都属于同一个包。
在C++中,我们使用 #include
来引入头文件,而在Golang中,我们使用 import
来引入包。
import "fmt"
#include <iostream>
另外,在Golang中,可以使用 import 圆括号
引入多个包,如下所示:
import (
"fmt"
"math"
)
上面的代码中,我们引入了 fmt
和 math
两个包。这个写法等价于:
import "fmt"
import "math"
- 导出名
- 在 Golang 中,如果一个标识符以大写字母开头,那么它就是导出的(public)。在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。
2.4. 函数
- Go 里面有三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者lambda函数
- 方法
- Golang 的函数需要用关键字
func
声明,而C++中的函数不需要关键字声明。 - Golang 中参数的类型在变量名之后,与变量名之间用空格隔开,这里和C++有所不同。
- 当两个或多个连续的函数命名参数具有相同的类型时,除最后一个类型之外,其他类型可以省略。
- Golang 函数的返回值类型在参数列表之后,与参数列表之间用空格隔开,C++11也支持这种尾置返回类型的写法,不过参数类型和参数列表之间用箭头
->
隔开,在C++17中,可以使用auto
关键字代替返回值类型。
func add(x int, y int) int {
return x + y
}
func add(x, y int) int { // 省略了 y 的类型
return x + y
}
auto add(int x, int y) -> int {
return x + y;
}
- Golang 函数的 { 不能单独放在一行。
func add(x, y int) int
{ // 错误
return x + y
}
- Golang 函数可以返回多个值,而C++中只能返回一个值(但C++中可以使用
std::tuple
或std::pair
来实现这样的效果)。- Golang 中的多返回值使用
圆括号
括起来。
- Golang 中的多返回值使用
func swap(x, y string) (string, string) {
return y, x
}
- Golang 不支持函数重载,也就是说不能定义名字相同而参数不同的函数。
- 命名返回值
- Golang 允许命名返回值,这种情况下,函数的返回值就会被视为定义在函数顶部的变量。这种情况下,函数的返回值可以被看作是函数的局部变量。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
- 尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。
- 函数是一等值(first-class value):它们可以赋值给变量,就像 add := binOp 一样。这个变量知道自己指向的函数的签名,所以给它赋一个具有不同签名的函数值是不可能的。
- 函数值(functions value)之间可以相互比较:如果它们引用的是相同的函数或者都是 nil 的话,则认为它们是相同的函数。
- 函数不能在其它函数里面声明(不能嵌套)
- 变长参数
- 和C++一样,Golang 中的函数可以接受任意数量的参数,这些参数被称为
变长参数
- defer
- Golang 中的 defer 语句会将函数推迟到外层函数执行return之后执行。
- 这个特性在 C++ 中是没有的。但是在 C++ 中可以使用 RAII 来实现类似的效果(在C++中,可以定义一个defer类, 在函数中初始化一个defer局部变量,这样在函数结束时就能够调用该defer的析构函数)
- 关闭文件流
- 解锁一个加锁的资源
- 打印最终报告
- 关闭数据库连接
- 代码追踪
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
func sum(nums ...int) {
fmt.Print(nums, " ")
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
- 匿名函数
- Golang 中的匿名函数可以作为闭包。闭包是一个函数值,它引用了函数体之外的变量。这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。
- Golang 中的匿名函数可以作为函数返回值,也可以作为函数参数。
- Golang 中的匿名函数可以作为立即执行函数(IIFE)。
2.5. 变量
- Golang 中的变量声明格式为
var <变量名> <变量类型>
,而C++中的变量声明格式为<变量类型> <变量名>
。-
Golang 将类型放到变量名后的原因: 为了避免像 C 语言中那样含糊不清的声明形式,例如:int* a, b;。在这个例子中,只有 a 是指针而 b 不是。如果你想要这两个变量都是指针,则需要将它们分开书写
-
- Golang 和 C++ 都支持多变量声明
- 在Golang中,被赋值的元素统一在等号的右边。
var a, b, c int
var d, e, f int = 1, 2, 3
- 在Golang中,交换两个变量的值非常方便,不需要使用中间变量。
a, b = b, a
int a, b, c;
int d = 1, e = 2, f = 3;
- 在Golang中变量如果没有被显示初始化,会被赋予
零值
, 所有的内存在 Go 中都是经过初始化的。
数值类型 | 字符串类型 | 布尔类型 | 指针类型 | 接口类型 | 切片类型 | 映射类型 | 通道类型 | 函数类型 |
---|---|---|---|---|---|---|---|---|
零值 | 0 | ”” | false | nil | nil | nil | nil | nil |
- Golang 中的
变量类型
不是必须的,当变量能够被编译器推断出来时,可以省略变量类型。
var a = 1
var b = true
var c = "Hello, World!"
在Golang中,还可以使用:=
来声明并初始化变量,这种方式只能在函数内部使用。
a := 1
b := true
c := "Hello, World!"
简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:
a := 1
a := 2 // 重复声明
Golang中可以使用因式分解var关键字
来声明多个变量。
var (
vname1 v_type1
vname2 v_type2
)
这种因式分解关键字的写法一般用于声明全局变量。
- 作用域
- Golang 中的变量的作用域和 C++ 中的变量作用域是一样的,指一个变量的作用范围。
- 局部变量
- 在函数体内声明的变量称之为局部变量。局部变量只能在函数内部使用。
- 全局变量
- 在函数体外声明的变量称之为全局变量。全局变量可以在整个包甚至外部包(被导出后)使用。
- 变量隐藏
- Golang 中允许在函数体内声明与外部变量同名的变量,此时,函数体内的变量会被优先考虑。这和C++中的变量隐藏是一样的。
package main
import "fmt"
var a = "G"
func main() {
var a = "O"
fmt.Println(a) // O
}
2.6. 常量
- Golang 中的常量使用
const
关键字声明,C++中的常量也是使用const
关键字声明。
常量的定义格式:const identifier [type] = value
const Pi = 3.14159
const a, b = 1, 2
- 在Golang中,常量可以使显示类型定义,也可以隐式类型定义。
- 显式类型定义: const b string = “abc”
- 隐式类型定义: const b = “abc”
- 和变量一样,常量也可以使用
因式分解关键字
的方式定义多个常量。
const (
vname1 v_type1 = v_value1
vname2 v_type2 = v_value2
)
2.7. 值类型和引用类型
- Golang 中的
值类型
包括:基本数据类型、数组和结构体。 - Golang 中的
引用类型
包括:指针、切片、映射、通道和接口。
2.8. 字符串
- 字符串拼接, Golang 使用
+
进行字符串拼接,和 C++ 一样。 - 在 Golang 中,获取字符串中的某个字符,可以使用
str[index]
的方式,和 C++ 一样。
2.9. 指针
- Golang 中的指针移动是不支持的
- Golang 中的指针只支持
&
和*
两个操作符,不支持++
和--
操作符。
- Golang 中的指针只支持
- Golang 中可以返回一个局部变量的指针,并且在函数外部使用这个指针。
- go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做
逃逸分析(escape analysis)
,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。所以不用担心会不会导致memory leak,因为GO语言有强大的垃圾回收机制。go语言声称这样可以释放程序员关于内存的使用限制,更多的让程序员关注于程序功能逻辑本身。
- go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做
- GOlang 中的 new 和 C++ 中的 new 有所不同
- Golang 中的 new 函数只分配内存,而不初始化内存,返回的是指向这个新分配的零值的指针。(Golang中的new 好像不一定把内存分配到堆区)
- C++ 中的 new 运算符不仅分配到堆内存,还初始化内存,返回的是指向这个新分配的对象的指针。
- 编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。
Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。
2.10. 控制语句
Go 完全省略了 if、switch 和 for 结构中条件语句两侧的括号,相比 Java、C++ 和 C# 中减少了很多视觉混乱的因素,同时也使你的代码更加简洁。(个人并不觉得,还是有括号看着舒服)
if-else
if condition1 {
// do something
} else if condition2 {
// do something else
} else {
// catch-all or default
}
-
关键字 if 和 else 之后的左大括号 { 必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号 } 必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。在C++中没有这种强制规定(但我一般就这么写的)
-
if 语句中可以定义一个变量,这个变量的作用域只在整个 if-else 语句块中。
- 这在C++17中也是可以的。
if v := math.Pow(x, n); v < lim {
return v
}
if (int v = math::pow(x, n); v < lim) {
return v;
}
switch
switch var1 {
case val1:
...
case val2:
...
default:
...
}
- Golang 中的 switch 语句不需要写 break,一旦匹配成功,自动终止。但C++中需要写 break,否则会继续执行下一个 case 语句。
for
- Golang 中的 for 循环只有一种形式,没有 while 和 do-while 循环。
- 基于计数器的循环
- 和 C++ 一样,Golang 中的 for 循环可以使用计数器的方式来实现。只是没有括号而已。
for i := 0; i < 10; i++ {
fmt.Println(i)
}
- 基于条件判断的循环
- Golang 中的 for 循环可以只写一个条件和 别的语言的 while 效果相同
for sum < 1000 {
sum += sum
}
- for-range 循环
- Golang 中的 for-range 循环可以用于遍历数组、切片、通道或映射。和C++中的 for-each 循环效果相同。
- 语法上很类似其它语言中 foreach 语句,但依旧可以获得每次迭代所对应的索引。一般形式为:for ix, val := range coll { }。
for i, v := range arr {
fmt.Println(i, v)
}
2.11. 数组与切片
- 数组声明
- Golang 中的数组声明格式为
var <数组名> [数组长度] <数组类型>
,而C++中的数组声明格式为<数组类型> <数组名>[数组长度]
。 - 通过new创建数组
- Golang 中可以使用
new
关键字来创建数组,返回的是数组的指针。
var arr = new([5]int) // arr 的类型是 *[5]int
- 切片
- Golang 中的切片是对数组的一个连续片段的引用,切片是一个引用类型,它的内部结构包含
地址
、长度
和容量
。 - 切片声明
- Golang 中的切片声明格式为
var <切片名> []<切片类型>
,而C++中没有切片这种数据结构。
var slice []int
切片的初始化格式为 var slice1 []type = arr1[start:end]。
- 使用make创建切片
- Golang 中可以使用
make
函数来创建切片,make
函数的格式为make([]T, len, cap)
,其中 T 为切片的元素类型,len 为切片的长度,cap 为切片的容量,cap这个参数是可选的。make
会在内存中分配一个数组,并返回一个切片,这个切片指向这个数组。 - new 和 make 的区别
- 看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
- new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
- make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。
2.12. Map
- Golang 中的 map 是一种无序的键值对的集合,和 C++ 中的 inordered_map 类似。
- map 是引用类型
- map 声明
- Golang 中的 map 声明格式为
var <map名> map[<键类型>]<值类型>
。
var map1 map[keytype]valuetype
var map1 map[string]int
- map 是引用类型, 使用make来分配内存
var map1 = make(map[keytype]valuetype)
- 不要使用new来创建map,因为new返回的是指向map的指针,而map是一个引用类型,所以不需要使用指针来访问map。
map 容量
map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。
我们可以在声明的时候指定 map 的容量,但是这只是一个性能调优,和数组的容量指定类似,不会限制 map 的长度。
make(map[string]int, 100)
用切片作为 map 的值
- Golang 中的 map 的值可以是切片,这样可以方便的实现多值映射。
var m = make(map[string][]int)
测试键值对是否存在及删除元素
val1 = map1[key1] 的方法获取 key1 对应的值 val1。如果 map 中不存在 key1,val1 就是一个值类型的空值。为了解决这个问题,我们可以这么用:val1, isPresent = map1[key1]
isPresent 返回一个 bool 值:如果 key1 存在于 map1,val1 就是 key1 对应的 value 值,并且 isPresent为true;如果 key1 不存在,val1 就是一个空值,并且 isPresent 会返回 false。
如果你只是想判断某个 key 是否存在而不关心它对应的值到底是多少,你可以这么做:
_, ok := map1[key1] // 如果key1存在则ok == true,否则ok为false
或者和 if 混合使用:
if _, ok := map1[key1]; ok {
// ...
}
在C++中,如果直接使用下标访问一个不存在的key,会自动插入一个默认值,而在Golang中,如果直接使用下标访问一个不存在的key,会返回一个默认值。
删除key
delete(map1, key1)
在C++中,删除一个key,可以使用
map1.erase(key1)
。
for-range 遍历 map
for key, value := range map1 {
fmt.Println(key, value)
}
和C++一样,可以使用for-range遍历map。
2.13. 包
- Golang 中的包是用来组织代码的,使用
import
关键字来引入包。