Golang 学习笔记

 

博主之前使用的语言主要是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"
)

上面的代码中,我们引入了 fmtmath 两个包。这个写法等价于:

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::tuplestd::pair 来实现这样的效果)。
    • 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 中可以返回一个局部变量的指针,并且在函数外部使用这个指针。
    • go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。所以不用担心会不会导致memory leak,因为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 关键字来引入包。