Go 语言的基础语法(下) | 青训营

152 阅读16分钟

书接上回,本篇将继续讲解Go语言的基础语法。

数组与切片

数组(array)

Go语言中的数组指的是长度固定,元素类型也固定的一组数据。数组是值类型(有默认值),其索引从0开始。Go语言中的数组支持使用 for…range 进行遍历,同时也支持占位符 '_'

注意:

  1. 注意长度不能留空,留空是切片类型。

  2. 长度是数组数据类型的一部分,因此数组长度不能改变,但等号右侧的长度可以简写为[…]自动判断。

以下是Go语言中数组常见的声明与遍历方式:

func main() {

    var a [3]int = [3]int{1,2,3}

    b := [3]int{3,4,5}

    var c = [3]int {
        5,
        6,
        7, //注意这种写法最后一个逗号不能省略
    }

    var d = [...]int{7,8,9}
    
    var twoDimensionalArray [3][4]int = [3][4]int{ //二维数组
        {1, 2, 3, 4},
        {2, 3, 4, 5},
        {3, 4, 5, 6},
    }

    a[0] = 100
    fmt.Println(a) //Go语言中可直接打印数组
    
    for i := 0; i < len(b); i++ {
        fmt.Printf("b[%v]=%v\n", i, b[i])
    }
    
    for i, v := range c {
        fmt.Printf("c[%v]=%v\n", i, v)
    }
    
    for _, v := range d { //此处的 '_' 为占位符
        fmt.Printf("%v\n", v)
    }
    
    for i, v := range twoDimensionalArray { //v接受的值为一维数组
        for i2, v2 := range v {
            fmt.Printf("a[%v][%v]=%v\t", i, i2, v2)
        }
        fmt.Println()
    }
}
  • 注意,与python不同,Go语言中的 range 仅用于对 slice、map、数组、字符串等进行迭代循环,不能用于指定for循环的次数或生成数字列表。

切片

切片是对数组的引用。切片本身并不存储任何数据,它只是描述了底层数组中的一段,其索引从0开始。其遍历方式与数组一致。若不想基于现有数组创建切片,也可以通过 make函数来创建切片。

切片是引用类型,其默认值为 nil ,改变切片的值同样也会改变其引用的数组(也可以是切片)的值。

切片可以用于函数接收多个参数。示例如下:

func SelectByKey(text ...string)(key int) {
    for i, v := range text {
        fmt.Printf("%v: %v\n", i+1, v)
    }
    fmt.Println("请选择:(数字)")
    fmt.Scanln(&key)
    return
}

切片常用函数

(以下列出的函数均为Go语言的内置函数,在调用时无需任何前缀)

  1. make函数。make函数的主要用途是用于分配相应类型的内存空间,其格式为 make([]Type(类型) len(长度) cap(容量))(注意此处 cap 的值应大于 len ,否则会报错;当省略 cap 时,cap 的值默认与 len 相等),且仅支持 slice(切片)、map(映射) 、channel(信道/管道)三种引用类型的内存创建,其返回值是所创建类型的本身,而不是新的指针引用。Go语言中的new函数与make函数功能相近,同样用于分配相应类型的内存空间,其格式为 new(Type) 但new函数除分配空间外同时会对类型进行初始化,且其返回值是所创建类型的指针引用
  2. len函数cap函数。len函数的作用是计算数组(包括数组指针)、slice 、map 、channel 、字符串等数据类型的长度,注意,结构体(struct)、整型布尔等不能作为参数传给len函数。cap函数则返回指定类型的容量。当向切片中元素个数超过容量时,切片会自动扩容。
  3. append函数。append函数的作用是向slice里面追加一个或多个元素(也可以追加切片),然后返回一个和slice一样类型的slice,此时改变返回切片的值不再影响原切片引用的数组。其格式为:append(slice []Type, elements...(可一次添加多个元素))
  4. copy函数。copy函数的作用是把一个切片内容复制到另一个切片中,超过目标切片容量的部分不予复制。其格式为:copy(目标切片, 源切片)

切片示例代码:

func main() {
    s := make([]string, 3) //注意 string 为字符串类型而非字符类型
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("len:", len(s)) // 3
    fmt.Println("cap:", cap(s)) // 3

    s = append(s, "d", "e")
    fmt.Println(s) // [a b c d e]
    fmt.Println("cap:", len(s)) // 5

    c := make([]string, len(s))
    copy(c, s)
    fmt.Println(c) // [a b c d e]

    fmt.Println(s[2:4]) // [c d],注意括号内范围左闭右开
    fmt.Println(s[:4])  // [a b c d]
    fmt.Println(s[2:])  // [c d e]

    good := []string{"g", "o", "o", "d"} //直接声明切片([]内无长度)
    fmt.Println(good) // [g o o d]
}

强制转换

Go语言中强制转换的语法为:转换后类型(变量),其中 string 也可与 []byte 相互转换。

map(映射)

Go语言里的 map 是一种无序的键值对( key-value )的集合。map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,不能重复,其指向数据的值。Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,map 是无序的,遍历 map 时返回的键值对的顺序是不确定的。在获取 map 的值时,如果键不存在,返回该类型的默认值。map 是引用类型,如果将一个 map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 map 的修改会影响到所有引用它的变量。(类似于切片)

  • 注意,map 中的 key 只能是基本数据类型:数字,字符串,布尔值。

以下是Go语言中map常见的声明与遍历方式:

func main() {
    m := make(map[string]int) //此处可省略初始size,默认为1(会自动扩容)
    m["one"] = 1
    m["two"] = 2
    fmt.Println(m)            // map[one:1 two:2]
    fmt.Println(len(m))       // 2
    fmt.Println(m["one"])     // 1
    fmt.Println(m["unknow"])  // 0

    r, ok := m["unknow"] // v用于取 value 的值,ok 的值可用来检查是否存在名为"unknow"的 key
    fmt.Println(r, ok)   // 0 false

    m2 := map[string]int{"one": 1, "two": 2}
    delete(m2, "one") //用于删除 map 中键值对
    fmt.Println(m2, m3) // map[two:2] map[one:1 two:2]
        
    var m3 = map[string]int{"one": 1, "two": 2}
    for key, value := range m1 {
        fmt.Printf("m3[%v]=%v\n", key, value)
    }
}
  • 注意,len函数对map无效。

自定义数据类型和类型别名(type)

自定义数据类型

定义格式:type 自定义数据类型 底层数据类型,这种定义下的自定义数据类型与底层数据类型属于不同类型,混用需要类型转换。

类型别名

定义格式:type 自定义数据类型 = 底层数据类型,这种定义下的自定义数据类型与底层数据类型属于相同类型,混用无需类型转换。

func TypeDefintionAndTypeAlias() {
    type mesType uint16 //自定义数据
    var u1000 uint16 = 1000
    var textMes mesType = mesType(u1000) //类型转换后赋值
    fmt.Printf("%v %T\n", textMes, textMes) // 1000 mesType
    
    type myUint16 = uint16 //类型别名
    var myu16 myUint16 = u1000 //直接赋值
    fmt.Printf("%v %T\n", myu16, myu16) // 1000 uint16
}

结构体

由一组字段构成的一种自定义数据类型,声明格式如下:

type 结构体名 struct {
字段名1 字段类型1
字段名2 字段类型2
...
}

结构体字段

结构体字段通过"."访问。值得一提的是,结构体可以没有字段,即空结构体。

结构体指针

注意,"."优先级高于"&"和"*",但在使用时可以简写(隐式间接引用):

type user struct {
	name     string
	password string
}

var a user
var b = &a

这种情况下,(*b).name 与 b.name 所表示的字段一致。

可以使用"&"前缀快速声明结构体指针。

组合

Go语言本身并不支持继承,但我们可以使用组合的方法,实现类似继承的效果。在Go语言中,把一个结构体嵌入到另一个结构体的方法,称之为组合。

type 结构体名 struct {
(*)组合的结构体名1
(*)组合的结构体名2
...
该结构体中其他字段
}

结构体标签(tag)

json 、bson 等格式进行序列化及对象关系映射(Object Relational Mapping,简称 ORM)系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为。其格式如下:

type 结构体名 struct {
字段名 字段类型 `标签:"字段别名"`
}

结构体示例代码:

type user struct {
    Name string `json:"name"` //注意,此标签意味着该变量需跨包调用,因此变量首字母应大写。注意:json标签的冒号后不能加空格
    password string
}

type account struct {
    user //也支持继承结构体指针(*user)
    id string
}

type contact struct {
    user
    remark string
}

func main() {
    a := user{Name: "wang", password: "1024"} //注意是冒号而非等号
    b := user{"wang", "1024"}
    c := user{Name: "wang"}
    c.password = "1024"
    var d user
    d.Name = "wang"
    d.password = "1024"
    var e = account{
        user: a, //若 account 中无与 user 相同的字段,则可直接通过 e.name 调用(相当于 e.user.name )
        id:   "2048",
    }
    var f = contact{
        user: user{
            Name: "wang",
            password: "1024",
        },
        remark: "100",
    }

    fmt.Println(a, b, c, d, e, f)
    // {wang 1024} {wang 1024} {wang 1024} {wang 1024} {{wang 1024} 2048} {{wang 1024} 100}
    fmt.Println(checkPassword(a, "1024"))   // true
    fmt.Println(checkPassword2(&a, "1024")) // true
}

func checkPassword(u user, password string) bool {
    return u.password == password
}

func checkPassword2(u *user, password string) bool {
    return u.password == password
}

方法(method)

方法指的是与特定类型绑定的函数。类型的定义和方法需要在同一个包内,因此方法一般用于自定义类型(包括结构体)。其声明格式为:func (接收参数名 类型) 方法名(形参列表) 返回值列表{},其中接收参数可以使用指针(此时可以在函数内修改接受参数的值)。其调用格式为:类型的实例.方法名(形参)

示例代码如下:

type user struct {
    name string
    password string
}

func (u user) checkPassword(password string) bool {
    return u.password == password
}

func (u *user) resetPassword(password string) {
    u.password = password
}

func main() {
    a := user{name: "wang", password: "1024"}
    a.resetPassword("2048")
    fmt.Println(a.checkPassword("2048")) // true
}

接口(interface)

接口是一种特殊的数据类型(动态类型),它是一种约束形式,其中只包括成员方法的定义,可以视为方法的集合。一个对象通过实现不同的接口可以灵活地完成很多任务。注意,接口本身不能绑定方法,接口是值类型,其保存的是:值 + 原始类型。

示例代码:

type Sleeper interface {
    Sleep()
}

type Dog struct {
    Name string
}

type Cat struct {
    Name string
}

func (d Dog) Sleep() {
    fmt.Printf("Dog %s is sleeping\n", d.Name)
}

func (c Cat) Sleep() {
    fmt.Printf("Cat %s is sleeping\n", c.Name)
}

func AnimalSleep(s Sleeper) { //接口可以作为形参
    s.Sleep()
}

func main() {
    var s Sleeper
    dog := Dog{Name: "xiaobai"}
    cat := Cat{Name: "xiaobai"}
    s = dog
    s.Sleep()
    s = cat
    AnimalSleep(s) //一般用这种方式来调用接口
}

下面是接口的一些常用操作:

  1. 类型选择,大致用法为:switch…case + 接口名.(type),注意:.(type)不能在 switch…case语句 外使用。示例如下:
func judgeType1(q interface{}) {
    switch i := q.(type) {
    case string:
        fmt.Println("这是一个字符串!", i)
    case int:
        fmt.Println("这是一个整数!", i)
    case bool:
        fmt.Println("这是一个布尔类型!", i)
    default:
        fmt.Println("未知类型", i)
    }
}
  1. 类型断言,作用是将接口类型还原为原始类型,其格式为:interface.(Type) 。如果接口没有保存类型,则会报错。此表达式可返回两个值:value, ok := interface.(Type)(此处也可以选择不用ok变量接受布尔值)。
func judgeType2(q interface{}) {
    temp, ok := q.(string)
    if ok {
        fmt.Println("类型转换成功!", temp)
    } else {
        fmt.Println("类型转换失败!", temp)
    }
}
  1. 空接口。没有任何方法的接口就是空接口(interface{})。Go语言的接口实现原则是:如果一个类型实现了某个接口的所有方法,就意味着这个类型实现了这个接口。实际上每个类型都实现了空接口,所以空接口类型可以接受任何类型的数据,即空接口可以被当作任何类型。在使用空接口变量时,需要通过断言将其解析为确定的类型。示例如下:
var i interface{}

i = []int{1, 2, 3}
fmt.println(i)

i = "abc"
fmt.println(i)

func test(foo interface{}) {
    switch foo.(type) {
    case string:
        str := foo.(string)
        fmt.Printf("%T\n", str) // string
    case []int:
        sl := foo.([]int)
        fmt.Printf("%T\n", sl) // []int
    }
}
  • 注意,不能将确定类型的切片直接赋值给空接口切片,需通过遍历逐个赋值。
  1. nil问题。这里需要区分两个概念:
  • nil 值:有类型没有值,接口本身并不是 nil,可以处理

  • nil 接口:既没有保存值,也没有保存类型,使用时会报错

Go语言的协程(Goroutine)

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)简介:

  • 进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。
  • 线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。 线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。
  • 协程是一种用户态的轻量级线程,又称微线程,其调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

Go语言的协程(Goroutine)是指在后台中运行的轻量级执行线程,协程是Go语言中实现并发的关键组成部分。区别于进程,线程,协程,因为Go语言的创造者们觉得Go语言的协程和他们是有所区别的,所以专门创造了Goroutine。

Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。它在程序中通过 go 关键字进行调用。

需要注意的是,当主线程结束时,协程会被中断,因此需要有效的阻塞机制,如使用 time.sleep()fmt.scanln() 等函数推迟主线程的结束时间。此外,还需要考虑数据竞争的问题,多个线程同时对同一个内存空间进行写操作会导致数据竞争,sync包中的互斥锁Mutex可以解决此问题,但在Go语言中不常用,因为go中有更高效的信道(channel)来解决这个问题。除此以外,各协程完成的先后顺序是随机的,可以通过函数或匿名函数来指定协程的相对顺序。

示例代码:

var (
    c int
    lock sync.Mutex
)

func PrimeNum(n int) {
    for i := 2; i < n; i++ {
        if n%i == 0 {
            return
        }
    }
    fmt.Printf("%v\t", n)
    lock.Lock()
    c++
    lock.Unlock()
}

func main() {
    for i := 2; i < 10001; i++ {
        go PrimeNum(i)
    }
    time.Sleep(5 * time.Second)
    fmt.Printf("\n共找到%v个素数\n", c)
}

channel(信道/管道)

Go语言中的 channel 是一种用于协程之间通信的机制。在并发编程中,协程之间的通信是非常重要的,因为它可以使得不同的协程之间协同工作,从而实现更高效的程序执行。

channel 是一种引用类型,使用前需要make(Type, (缓冲区容量)),其中缓冲区的容量可省略,不带缓冲区的 channel 必须结合结合协程使用。示例:ch := make(chan string) //其中 chan 是 channel 的简写,string 表示传递数据的类型。channel 可以查看长度(len)和容量(cap)。

channel 是线程安全的,多个协程可以同时读写一个 channel ,而不会发生数据竞争的问题。这是因为Go语言中的 channel 内部实现了锁机制,保证了多个协程之间对 channel 的访问是安全的。

channel 中的数据先进先出,自动阻塞。因此数据需要保持流动,否则会阻死报错。

channel 常用操作如下:

  • 存入: channel <- value
  • 取出: value, (ok) <- channel // ok可以用来检查 channel 是否已经被关闭了
  • 丢弃:<- channel
  • 关闭:close(channel) // channel关闭后数据不能流入但可以流出,若 channel 为空则返回默认值

示例代码:

func main() {
    go func() {
        time.Sleep(1 * time.Second)
    }() //此括号表示对匿名函数直接进行调用
    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)
    }()
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("Finished")
}
  • for 或 for…range 语句可以不断从信道接收值,直到它被关闭(缺乏关闭机制会报错)。
  • 当无法确认适合关闭信道的情况时,可以用 select...case 语句,该语句会阻塞到某个分支可以继续执行时执行该分支,当没有可执行的分支时则执行 default 分支,其一般与 for 循环结合使用。其格式如下:
select {  
  case <- channel1:  
    // 执行的代码  
  case value := <- channel2:  
    // 执行的代码  
  case channel3 <- value:  
    // 执行的代码  
  ...
  default:  
    // 所有通道都没有准备好,执行的代码(相对的,若有多个通道准备好,则从中随机执行一个)
}

示例代码:

func main() {  
  // 定义两个通道  
  ch1 := make(chan string)  
  ch2 := make(chan string)  
  
  // 启动两个 goroutine,分别从两个通道中获取数据  
  go func() {  
    for {  
      ch1 <- "from 1"  
    }  
  }()  
  go func() {  
    for {  
      ch2 <- "from 2"  
    }  
  }()  
  
  // 使用 select 语句非阻塞地从两个通道中获取数据  
  for {  
    select {  
    case msg1 := <-ch1:  
      fmt.Println(msg1)  
    case msg2 := <-ch2:  
      fmt.Println(msg2)  
    default:  
      // 如果两个通道都没有可用的数据,则执行这里的语句  
      fmt.Println("no message received")  
    }  
  }  
}

除此以外,select...case 语句还有一个很重要的应用就是超时处理。 因为上面我们提到,如果没有 case 需要处理,且没有 default 语句select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。常用的方法有 time.After 方法等。

以上就是Go语言基础语法的全部内容了,感谢您的阅读。₍ᐢ..ᐢ₎♡