[开发语言 | Go] 01 - 基础语法

124 阅读3分钟

我正在参加「掘金·启航计划」

1. 包、变量和函数

  1. 导入多个包时,可以用两种方式:
import "fmt"
import "math"

或者

import (
	"fmt"
	"math"
)
  1. 在Go中,以大写字母开头的名字属于导出的名字,当导入一个包时,只能访问其中导出的名字,任何未导出的名字无法在包的外部访问。
  2. 类型名在变量名之后。

Rob Pike解释了这样设计的原因:以独立的语法为代价换取更清晰的表达。

C语言中使用的语法会导致复杂的表达式难以解析,而Go的语法类似于:

x: int

p: pointer to int

a: array[3] of int

只有指针是个例外,对比 slice:var a []intx = a[1]

指针的类型语法和表达式语法相同:var p *intx = *p

所以在类型转换时,slice可以使用[]int("hi"),但是指针的类型必须加括号(*int)(nil)避免造成混淆。

  1. 当函数有多个连续的命名形参类型相同时,可以省略最后一个之前的类型声明,例如:x int, y intx, y int
  2. 一个函数可以返回任意数量的结果
  3. Go函数的返回值可以是命名的,这些命名返回值被视作定义在函数顶部的变量。没有参数的return语句将返回命名返回值,称为直接返回。为了可读性,直接返回应当只用在短函数中。
func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}
  1. var可以声明一组变量,这些变量可以是包/函数级别的。
  2. 变量声明可以包含初始值,如果存在初始值,变量的类型可以省略(使用初始值的类型)。
  3. 在函数中,变量的短赋值语句:=可以代替var声明;但是在函数外,所有语句都以关键字开头,所以不能使用:=语句。
func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}
  1. Go的基本类型如下。int/uint/uintptr根据系统的位数可依是32-bit或者64-bit。
bool
string
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
     // represents a Unicode code point
float32 float64
complex64 complex128

变量也可以成组声明:

var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)
  1. 没有显式初始化的变量会被赋予对应的零值。如0/false/""
  2. 语句T(v)会将变量v转换为类型TGo必须进行显式的类型转换
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
  1. 声明变量时如果省略了类型,那么变量的类型将根据右值进行推断。当右值声明了类型时,类型与右值一样;如果右值包含未声明类型的常量,新变量的类型将取决于常量的精度。
  2. 常量使用const进行声明,方法与变量一样,但不可使用:=
  3. 数值常量是高精度的值,未指明类型的数值常量将根据上下文决定其类型。如下所示,注意int变量最多只能存储64-bit的整数。
const Big = 1 << 100

2. 流程控制语句

  1. Go只有一种循环结构,就是for循环。
  2. for循环结构包含三部分:初始化语句(第一次循环之前执行)、条件表达式(每次循环之前执行)和后置语句(每次循环之后执行),其中初始化语句声明的变量只在for作用域内可见。这三部分没有小括号包围,但是循环体的大括号{}是必需的。
  3. 初始化语句和后置语句是可选的,如果两者不存在还可以进一步省略分号。因此在Go中,for循环可以实现C语言中while循环的作用。
func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
}
  1. 如果进一步去掉条件表达式,可以得到一个无限循环。
  2. Go的if语句与for循环类似,条件表达式无需小括号,但是大括号是必需的,关键词必须和大括号在同一行。在条件表达式之前可以执行一个简单语句,其中声明的变量仅在if的作用域(包括其它else分支)内可见。
func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else if v == lim {
		fmt.Printf("%g == %g\n", v, lim)
	} else {
		fmt.Printf("%g > %g\n", v, lim)
	}
	// can't use v here, though
	return lim
}
  1. Go中的switch语句:只会执行选中的case,相当于每个case之后默认加了break;每个case不必是常量,也不必是整数;
  2. 没有条件表达式的switch语句相当于switch true,可以用于替代冗长的if-then-else语句。
func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.\n", os)
	}
}
  1. defer语句会将函数推迟到外层函数返回之后执行。推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
func main() {
	fmt.Println("counting")
	for i := 0; i < 3; i++ {
		defer fmt.Println(i)
	}
	fmt.Println("done")
}
// 输出 done 2 1 0

3. 更多类型

  1. 指针持有值的内存地址,取地址/解引用与C语言类似。Go的指针没有算术运算。
var p *int
i := 42
p = &i
*p = 21
  1. 结构体是字段的集合,其中的字段通过点运算符访问。也可以使用结构体的指针访问字段,使用时可以省略解引用,用p.X代替(*p).X
  2. 结构体字面量通过直接列出字段的值来分配一个结构体,使用Name:语法可以只列出部分字段。
type Vertex struct {
	X int
	Y int
}

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

func main() {
	v := Vertex{1, 2}
	p := &v
	p.X = 1e9
	fmt.Println(v)
}
  1. 类型[n]T表示包含n个T类型值的数组(array) 。数组的大小属于数组类型的一部分,所以数组大小固定。
  2. 类型[]T是包含T类型元素的切片(slice) 。切片可以看作数组的引用,为数组提供了一种动态的视图(view)。
  3. 切片不存储任何数据,它只描述一个潜在数组的片段,由两个索引指定的左闭右开区间组成a[low:high]
  4. 修改切片的元素会修改其对应的潜在数组的元素,而与其共享潜在数组的其它切片能看到这些修改。因此处理共享潜在数组的切片时要格外小心
  5. 切片的字面量与数组字面量类似,但是没有长度。数组的字面量:[3]bool{true, true, false},切片的字面量:[]bool{true, true, false}。后者会首先创建一个与前者一样的数组,然后生成引用该数组的切片。
  6. 使用切片时省略边界索引会采用默认值:下边界为0,上边界为切片的长度。
  7. 切片有长度和容量两个不同的属性。长度len(s)代表切片中的元素个数;容量cap(s)表示从切片的第一个元素开始计算,其潜在数组中的元素个数。
  8. 切片的零值是nil,长度和容量均为0,没有潜在的数组。

var s []intsnil

s := []int{}s不是nil

s := make([]int, 0)s不是nil

  1. 切片可以包含任何类型,包括其它切片。
  2. make函数通过创建一个置零的数组并返回其引用来创建切片。可以通过参数指定长度和容量。
  3. append函数可以向切片中增加新的元素并将新的切片返回。如果切片原来的潜在数组大小不足,将会自动分配一个新的潜在数组并返回对应切片。注意原来共享潜在数组的切片此时不再共享。
  4. for循环的range形式可以遍历切片或映射。当遍历切片时,每次迭代会返回两个值,一个是索引,一个是索引对应元素的拷贝,可以使用_跳过索引或者元素值,需要时也可以只保留索引。

for i, _ := range pow / for _, value := range pow / for i := range pow

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}
    a := primes[1:4]		// (1)
	
    var b []int				// (2) nil
    
    c := []int{2, 3, 5}		// (3)
    c = c[:2]
    c = c[1:]

	d := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}
    
    e := make([]int, 5)		// (4)
    f := make([]int, 0, 5)	// (5)
    
    f = append(f, 2, 3, 4)
}
  1. 类型map是键-值对的集合。
  2. map的零值是nil,没有键,也不能添加键。
  3. make函数可以初始化一个可以使用的给定类型的map并返回。
  4. map的字面量与结构体类似,但是需要键。可以省略字面量元素中的类型名。
  5. 插入或更新map的元素:m[key]=elem
  6. 检索map的元素:elem = m[key]
  7. 删除map的元素:delete(m, key)
  8. 检查键是否存在:elem, ok = m[key]。如果key不存在,okfalseelem为元素类型的零值;如果key存在,oktrueelem为对应的元素。
type Vertex struct {
	Lat, Long float64
}
var m1 map[string]Vertex	// nil
var m2 = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}

func main() {
	m3 := make(map[string]int)
	m3["Answer"] = 42
}
  1. 函数也是一种值,可以作为函数的参数和返回值。
  2. Go函数可以是闭包。闭包引用了函数体之外的变量,可以访问并赋值引用的变量。该函数与这些变量绑定在一起。
func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

4. 空值

  1. 未显式初始化的变量会被赋予一个对应类型的空值。
  2. 不同类型对应的空值不同:
    1. 值类型:布尔类型false,数值类型0,字符串"",数组和结构体会递归地初始化元素和字段。
    2. 应用类型:包括指针、函数、接口、信道、切片、map,均为nil