Go 语言入门指南:基础语法和常用特性解析 | 青训营

90 阅读12分钟

Go语言(又称为Golang)是一种开源的编程语言,由Google开发,于2009年首次公开发布。它被设计用于构建高效、可靠和简洁的软件。Go语言的设计目标之一是提供一种具有高并发性、高性能和易于编写的语言,适用于各种领域的应用开发。

Go语言内置了并发编程的支持,通过goroutine和channel,开发者可以轻松实现高并发的程序;Go语言使用自动垃圾回收机制,开发者无需手动管理内存;Go语言的语法设计简单且清晰,易于阅读和编写;Go语言附带了一个丰富的标准库,涵盖了网络、文件操作、加密、并发等各种功能。由于其优秀的性能和并发支持,Go语言在构建Web应用程序、后端服务、分布式系统等领域得到了广泛应用。

本文将讲解Go语言的主要语法和特性。

1 Package

Package在Go语言中有着重要的作用。它可以用于将相关的函数、结构体、常量和变量组织在一起,形成一个独立的功能单元。Package的模块化特性有助于代码的可读性、维护性和重用性。在Go程序中,特殊的package叫做"main"包。一个Go程序必须包含一个"main"包,其中包含了程序的入口函数main(),在运行程序时会首先执行这个函数。

// main.go
package main
​
import "fmt"func main() {
    fmt.Println("Hello world!")
}

通过使用packages,可以更好地组织和管理你的Go代码,使得代码结构更清晰和模块化。

2 变量和函数

在Go语言中,可以使用关键字来声明变量和函数。

var关键字用于声明变量,func关键字用于声明函数。在声明变量时,你可以选择是否初始化变量的值。在声明函数时,你需要指定函数名、参数列表和函数体。在Go中,函数可以返回多个值,而且还可以将函数作为值进行传递,实现匿名函数等。

变量和函数的命名遵循驼峰命名法,首字母大小写决定了其可见性,以大写字母开头的标识符对外部包是可见的。在函数内部,你可以使用:=来进行短声明并初始化变量。

var num int = 5func sum(newNum int) int {
    retVal := newNum + num
    return retVal
}

Go语言是静态类型语言,变量在声明时必须指定其类型。Go语言的类型系统非常严格,变量的类型在编译时就确定了,并且不能在运行时更改。类型的严格定义有助于编写可靠和高性能的代码。声明变量时,可以使用短变量声明(:=)来让编译器自动推断变量类型,但在函数外部必须使用var关键字进行显式声明。

Go有13种变量类型。

2.1 整数类型

  • int:根据系统架构可能是32位或64位。
  • int8int16int32int64:分别表示8、16、32、64位有符号整数。
  • uintuint8uint16uint32uint64:无符号整数。
var intValue int = 10
var uintValue uint = 20

2.2 浮点数类型

float32float64:分别表示32位和64位浮点数。

var floatValue float32 = 3.14
var doubleValue float64 = 6.28

2.3 复数类型

  • complex64:由两个float32构成的复数。
  • complex128:由两个float64构成的复数。
var complexValue complex64 = 1 + 2i
var biggerComplexValue complex128 = 2 + 3i

2.4 字符串类型

string:表示字符串,由一系列字符组成。

var stringValue string = "Hello, Go!"
  • 使用len函数可以获取字符串的长度。
  • 使用==!=操作符可以比较两个字符串是否相等。
  • 使用+操作符可以连接两个字符串。
  • 使用strings.Split函数可以根据指定的分隔符将字符串分割为切片。
  • 使用strings.Replace函数可以替换字符串中的指定子串。
  • 使用strings.Contains函数可以判断字符串是否包含指定子串。
  • 使用strings.HasPrefixstrings.HasSuffix函数可以判断字符串是否以指定的前缀或后缀开始或结束。
  • 使用strings.ToLowerstrings.ToUpper函数可以将字符串转换为小写或大写形式。

2.5 布尔类型

bool:表示布尔值,只有两个值:truefalse

var boolValueTrue bool = true
var boolValueFalse bool = false

2.6 指针类型

*T:指向T类型的指针。

var intPointer *int
intPointer = &intValue

2.7 数组类型

Go语言中的数组是一种固定大小、元素类型相同的数据结构,用于存储一组有序的数据。数组在声明时需要指定数组的长度,一旦确定长度后,数组的大小就不能再改变。声明数组时需要指定数组的长度和元素类型。

[n]T:表示n个元素的数组,其中每个元素的类型为T。

var intArray [3]int
intArray = [3]int{1, 2, 3}
  • 使用len函数可以获取数组的长度。

  • 使用range关键字来遍历数组,可以同时获得元素索引和值。

    for index, value := range arr {
        fmt.Printf("Index: %d, Value: %d\n", index, value)
    }
    
  • 使用==操作符可以比较两个数组是否相等。

数组在Go语言中是一个较为基础的数据结构,适用于需要存储固定大小数据集合的情况。但由于数组的长度一旦确定就无法改变,因此在需要动态大小的数据集合时,更常用的是切片(Slice)类型。切片是基于数组构建的动态长度的数据结构,更灵活地适应各种场景。

2.8 切片类型

在Go语言中,切片(Slice)是一种动态长度的数据结构,它基于数组构建而成,可以自动调整大小。切片提供了方便的方法来操作序列化数据,使得在处理动态大小的集合时更加灵活和高效。当切片的长度超过容量时,切片会自动扩容,会重新分配更大的底层数组。

[]T:表示一个动态大小的切片,其中每个元素的类型为T。

在Go语言中,make函数用于创建动态数据结构,如切片、映射(map)和通道(channel)。make函数返回一个初始化后的、可使用的数据结构。在切片的情况下,make函数除了指定切片的类型和长度外,还可以指定切片的容量。切片的长度是切片当前存储的元素数量,容量是切片底层数组的大小。切片在增长时可能会重新分配底层数组,但在容量不足的情况下,仍然可以从旧的底层数组引用中重新分配。

在映射和通道的情况下,make函数主要用于初始化一个新的映射或通道,并返回可以使用的实例。对于数组类型,不需要使用make函数来创建,而是直接通过初始化数组来声明。make函数只用于动态数据结构的创建。

var intSlice = make([]int, 5)

可以通过数组或其他切片来初始化切片。

var intArray = [5]int{1, 2, 3, 4, 5}
var intSlice = intArray[:]
  • 使用append函数可以向切片末尾添加元素。

  • 使用len函数可以获取切片的长度,使用cap函数可以获取切片的容量。

    多个切片可以引用同一个底层数组,这使得切片可以共享数据。

2.9 映射类型

map[K]V:表示具有键类型K和值类型V的映射(字典)。

var keyValueMap map[string]int
keyValueMap = map[string]int{"one": 1, "two": 2}
  • 使用delete函数来删除map中的键值对。
  • 使用 _, ok := map[key]来检查某个键是否存在于map中。若 oktrue, 则存在。
  • 使用range关键字遍历map的键值对。
  • 使用len函数获取map中的键值对数量。

2.10 函数类型

func:表示函数类型,可以是参数或返回值的类型。

var addFunction func(int, int) int
addFunction = func(a, b int) int {
    return a + b
}

在Go语言中,如果一个函数接受指针类型参数,当你传入该类型的值时,Go会自动为你创建一个指向该值的指针,并将这个指针传递给函数。这种转换是隐式的,开发者无需手动创建指针。反之同样成立。如果一个函数接受非指针类型参数,当你传入该类型的指针时,Go会自动解引用指针并传递其所指向的值。

2.11 结构体类型

struct:可以包含不同类型的字段的复合数据类型。

type Rectangle struct {
    Width  float64
    Height float64
}
var rect Rectangle = Rectangle{Width: 16.0, Height: 8.0}

2.11.1 方法

在Go语言中,方法(Methods)是指与特定类型关联的函数,它们可以在类型的实例上调用。方法可以用于为用户自定义类型(结构体)添加行为和功能,使得这些类型可以拥有自己的行为。方法的定义与普通函数类似,但是它们需要在函数名前面加上接收者(Receiver),表示方法是与哪个类型关联的。

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

通过指针类型的接收者,方法可以修改实例的值。

func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}
​
rect := &Rectangle{Width: 5, Height: 3}
rect.Scale(2)

2.12 接口类型

接口在编程中的作用非常重要。接口可以实现多态性,使不同类型的对象可以被统一对待。通过接口,你可以编写通用的代码,处理不同类型的对象,而无需关心具体的类型。这样可以提高代码的可复用性和灵活性;接口可以将接口定义和实现分离,降低了不同模块之间的耦合度。模块可以根据接口定义进行开发,而不必知道具体实现细节,这样可以提高代码的可维护性和可扩展性。举个例子,假设你有一个图形计算程序,可以计算不同形状的面积和周长。如果使用接口,你可以定义一个Shape接口,其中包含计算面积和周长的方法。然后,你可以针对不同的形状(如矩形、圆形)创建实现了Shape接口的类型。这样,你可以使用相同的方法名来处理不同的形状,提高了代码的灵活性和可维护性。

在Go语言中,接口(Interface)是一种特殊的数据类型,用于定义一组方法的集合。接口描述了对象应该具备的行为,而不关心对象的具体类型。通过实现接口,不同的类型可以拥有相同的方法集合,从而实现了多态性和代码的可复用性。在Go中,接口是一组方法签名的集合,不包含实现的具体代码。方法签名由方法名、参数列表和返回值组成。

type Shape interface {
    Area() float64
    Perimeter() float64
}

任何类型只要实现了接口中的所有方法,就被视为实现了该接口。接口的实现是隐式的,无需显式声明。在下面的例子中, Rectangle 实现了接口 Shape

type Rectangle struct {
    Width  float64
    Height float64
}
​
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}
​
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

2.13 通道类型

chan T:表示一个通道,用于在协程之间进行通信。

var intChannel chan int
intChannel = make(chan int)

3 流程控制

Go语言的流程控制包括条件语句(if、switch)、循环语句(for)、以及跳转语句(break、continue、goto)。在Go语言中,流程控制语句的使用方式与其他编程语言类似,但Go强调简洁性和可读性。所有的条件判断语句都不带括号。

func main {
    // if
    x := 10
    if x > 5 {
        fmt.Println("x is greater than 5")
    } else if x == 5 {
        fmt.Println("x is equal to 5")
    } else {
        fmt.Println("x is less than 5")
    }
​
    // switch
    day := "Monday"
    switch day {
    case "Monday":
        fmt.Println("It's the start of the week")
    case "Friday", "Saturday":
        fmt.Println("It's the weekend")
    default:
        fmt.Println("It's a regular day")
    }
    
    // for
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }    
}

4 并发

Go语言是一门在设计时就考虑了并发的编程语言,它提供了轻量级的并发支持,使得编写并发程序变得简单和高效。Go使用了一种称为 goroutine 的机制来实现并发,同时还提供了 channel 来进行数据的同步和通信。

4.1 Goroutine

Goroutine是Go语言中的并发执行单位,相当于一种轻量级的线程。与传统线程相比,goroutine的创建和切换开销非常小,可以同时运行成千上万个goroutine。使用go关键字可以启动一个新的goroutine。

func main() {
    go doSomething()
}

Go语言使用了CSP(Communicating Sequential Processes)并发模型,它将并发的实现抽象为独立的goroutine,通过通道进行通信和同步。这种模型使得编写并发代码更加直观和安全;Go语言内置了并发安全的机制,包括互斥锁、读写锁等。这些机制帮助开发者在多个goroutine访问共享数据时避免竞态条件和数据竞争。

4.2 协程间通信

Channel是用于在不同goroutine之间传递数据的管道。通道主要用于协程之间的数据交换和同步操作。它可以用于解决并发访问共享数据的问题,确保数据的同步和一致性。通道还可以用于协程之间的通信,实现协程的协调和协作。

使用make函数可以创建一个通道,并指定通道中的数据类型。 通过调用close函数可以关闭一个通道。关闭通道后,无法再向通道发送数据,但仍然可以从通道接收数据。

ch := make(chan int) // 创建一个整数类型的通道
close(ch) // 关闭通道

使用<-运算符将数据发送到通道、从通道接收数据。

ch <- 42 // 将整数值 42 发送到通道
value := <-ch // 从通道接收数据并存储到变量 value 中

通道的发送和接收操作都是阻塞的,意味着在发送数据时如果通道已满,发送操作会一直阻塞,直到通道有空间。同样,在接收数据时如果通道为空,接收操作也会一直阻塞,直到通道有数据。

通道具有容量限制,这是指通道可以同时存储的元素数量。通道的状态包括是否关闭以及当前存储的元素数量。

ch := make(chan int, 5) // 创建容量为 5 的整数类型通道
ch <- 1 // 发送数据
len(ch) // 返回通道中的元素数量
cap(ch) // 返回通道的容量