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

141 阅读16分钟

Go关键字

Go语言中共有25个关键字。

break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

预定义名称列表。

// 内建常量 
true false iota nil

// 内建类型 
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
float32 float64 complex128 complex64
bool byte rune string error

// 内建函数
make len cap new append copy close delete
complex real imag
panic recover

Hello World

package main

import "fmt"

func main(){
    fmt.Println("Hello World")
}

在以上Go语言示例代码中,主要由三部分包含组成:

第一部分导入包名,默认为 main ;

第二部分导入标准库;

第三部分 main函数定义,在go语言中,代码如果需要跑起来,代码必须要有main函数作为入口。

习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字由几个单词组成时优先使用大小写分隔,而不是优先用下划线分隔。

package

package指归属于哪个包,go语言中以目录结构叫做包。 如果代码中有main()函数,当前package包必须是 main包。

package main

import

导入标准库或第三方库

import "fmt" // 支持单个

import (     // 支持多个
	"log"
    alias "other/xxxx" // 支持库设置别名
    _ "other/xxxs" // 支持库默认加载库
)

声明、赋值

Go语言主要有四种类型的声明语句:var、const、type 和 func,分别对应变量、常量、类型和函数实体对象的声明。

在使用多重赋值时,如果想要忽略某个值,可以使用 匿名变量(anonymous variable) 匿名变量用一个下划线 _ 表示。

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。

var

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。

变量声明的一般语法:

// var 变量名字 类型 = 表达式
var s string = "字节青训营"

数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil

也可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化。

var f, err = os.Open(name) // os.Open returns a file and an error

Go 语言中变量的声明必须使用空格隔开。

var age int

定义字符串数组语法。

var strArrar1 []string // 声明类型为string数组
var strArrar2 []string{"a", "b"} // 并初始化值

const

常量表达式的值在编译期计算,而不是在运行期。每种常量的潜在类型都是基础类型:boolean、string或数字。 常量的值不可修改,这样可以防止在运行期被意外或恶意的修改。

// 声明
const num = 10

type

新命名的类型,用来分割不同概念的类型,这样即使它们底层类型相同也不是兼容的。

// type 类型名字 底层类型
type Num float64

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

func

函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。

func name(param) (result) { 
    // body 
}

函数传参,返回值 float64类型。

func hypot(x, y float64) float64 {
  return math.Sqrt(x*x + y*y)
}

defer用法

defer 延迟调用, 一般用在函数中最后执行。

func example() {
    defer func() {
        fmt.Println("最后执行")
    }
    fmt.Println("首先执行")
}

引用类型

  • slice
  • map
  • channel

slice

介绍

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。一个slice由三个部分构成:指针、长度和容量。 指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。

和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。

一个零值的slice等于nil。 一个nil值的slice并没有底层数组。一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。与任意类型的nil值一样,我们可以用[]int(nil)类型转换表达式来生成一个对应类型slice的nil值。

使用

使用内建make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

make([]T, len) // 省略容量
make([]T, len, cap) // 容量

使用内建append函数可以追加多个元素,甚至追加一个slice。

var x []int
x = append(x, 1)
x = append(x, 2, 3)
x = append(x, 4, 5, 6)

map

介绍

哈希表是一种巧妙并且实用的数据结构。 它是一个无序的key/value键值对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

在Go语言中,一个map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。

map类型的零值是nil, 也就是没有引用任何哈希表。

使用

使用内建make函数创建一个map。

ages := make(map[string]int)	

可以用map字面值的语法创建map,同时还赋值key/value。

ages := map[string]int{"age1": 18, "age2": 19}

另一种创建空的Map表达式。

ages := map[string]int{}

Map中元素通过key下标语法访问。

ages["age1"] = 18
fmt.Println(ages["age1"])

使用内建delete函数可以删除元素。

delete(ages, "age1")  // 即使某些元素不在map中也没有关系,失败时将返回0。

遍历map中全部的key/value对的话,可以使用range风格的for循环实现。

for name, age := range ages {
    fmt.Printf("%s\t%d\n", name, age)
}

禁止对map元素取地址,是因为map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

channel

介绍

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义,channel就是它们之间的连接。

channel可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

注意:goroutine 是go语言中特有的机制,可以理解为go语言中的线程。 使用goroutine, 可以使用go关键字

go并发方面的知识会放到后续文章中,和大家分享。 此知识点中简单了解即可

使用

channel声明

channel是一种类型,一种引用类型。语法格式:

var 变量 chan 元素类型

例子:

 var ch1 chan int   // 声明一个传递整型的通道
 var ch2 chan bool  // 声明一个传递布尔型的通道
 var ch3 chan []int // 声明一个传递int切片的通道    

创建channel

通道是引用类型,通道类型的空值是nil。

声明的通道后需要使用make函数初始化之后才能使用。

make(chan 元素类型, 缓冲大小) // 格式

例子:

ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用 <- 符号。

发送

将一个值发送到通道中。

ch := make(chan int)
ch <- 100 // 把100发送到 ch 中

接收

从一个通道中接收值。

x := <- ch // 从ch通道中接收, 并赋值 x
<- ch // 从ch通道中接收, 忽略值 

关闭

调用内建 close 函数来关闭通道。

close(ch)

关闭后的通道有以下特点:

  1. 对一个关闭的通道再发送值就会导致panic
  2. 对一个关闭的通道进行接收会一直获取值直到通道为空
  3. 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值
  4. 关闭一个已经关闭的通道会导致panic

需要注意:只有在通知接收方 goroutine 所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

channel分类

无缓冲通道

无缓冲的通道又称为阻塞的通道

无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

使用 ch := make(chan int) 创建的是无缓冲的通道。

func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
}   

上面这段代码能够通过编译,但是执行的时候会出现死锁错误。

fatal error: all goroutines are asleep - deadlock!

一种方法是启用一个 goroutine 去接收值。

func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}   

无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲通道

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

使用 make函数 初始化通道的时候为其指定通道的容量。

func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

从通道循环取值案例。

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    // 开启goroutine将0~100的数发送到ch1中
    go func() {
        for i := 0; i < 100; i++ {
            ch1 <- i
        }
        close(ch1)
    }()
    // 开启goroutine从ch1中接收值,并将该值的平方发送到ch2中
    go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()
    // 在主goroutine中从ch2中接收值打印
    for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }
}   

单向通道

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

单向通道案例:

func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}  

其中:

  1. chan<- int 是一个只能发送的通道,可以发送但是不能接收;
  2. <-chan int 是一个只能接收的通道,可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。

指针

介绍

区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。

Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。

Go语言中的函数传参都是值拷贝, 当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。

传递数据使用指针,而无须拷贝数据。

Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。

Go语言中的值类型 (int、float、bool、string、array、struct) 都有对应的指针类型,如:*int、*int64、*string等。

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

使用

取变量指针的语法如下:

var v int = 10
ptr := &v
fmt.Printf("v:%d ptr:%p\n", v, ptr)

c := *ptr // 指针取值(根据指针去内存取值)
fmt.Printf("c:%d\n", c)

v: 代表被取地址的变量,ptr: 用于接收地址的变量,ptr的类型称做int的指针类型。*代表指针。

程序定义一个int变量num的地址并打印

将a的地址赋给指针p,并通过p去修改a的值。

func main() {
    var a int
    fmt.Println(&a) // 指针地址
    var p *int
    p = &a  // 等同于 p := &a
    *p = 20
    fmt.Println(a) // 输出 20
}

空指针的判断:

func main() {
    var p *string
    fmt.Printf("p的值是%v\n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}

结构体

介绍

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体,每个值称为结构体的成员。

通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行。

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。 一个结构体可能同时包含导出和未导出的成员。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)

结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。

使用

type Info struct {  // 创建结构体
    ID	  int
    Name, Hobby  string
}
var info Info // 声明结构体

结构体变量的成员可以通过点操作符访问。

info.Name = "Alice"

对成员取地址,然后通过指针访问。

name := &info.Name
fmt.Println(*name)

如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

func UpdateInfo(i *info) {
    i.Hobby = "sing"
}

通过指针创建并初始化一个结构体变量,并返回结构体的地址。

pp := &Point{1, 2}

它和下面的语句是等价的。

pp := new(Point)
*pp = Point{1, 2}

注:结构体也是可以比较的,两个结构体将可以使用或!=运算符进行比较。相等比较运算符将比较两个结构体的每个成员。

控制语句

if判断

单条件判断。

if condition {
    // do something
}

多条件判断。

if condition {
    
} else if condition {
    // do something
} else {
    // do something
}

if 单条件先跟个语句然后再做条件判断。

if statement;condition{
    //do something
}

多条件带语句判断。

if num := 78;num <= 50{
    fmt.Println("Number is less then 50")
} else if num >= 51 && num <= 100{
    fmt.Println("The number is between 51 and1 100")
} else{
    fmt.Println("The number is greater than 100")
}

for循环

go语言中只有一种循环方式,for循环。 第一种语法格式。

for 循环变量初始化;循环条件;循环变量迭代 {
	// 循环操作(语句)
}

for j := 1; j <= 10; j++ {
    // 循环执行语句
}

第二种语法格式。

for 循环判断条件 {
    // 循环执行语句
}

j := 1
for j <= 10 {
    j++
}

第三种语法格式。

for {
    // 循环执行语句,是一个无限循环,通常需要配合 break 语句使用
}

遍历式语法格式 for-range。

var names []string{"xiaoming", "xiaohong", "xiaojun"}
for _, name := range names {
    fmt.Println(name)
}

使用 goto 语句跳出 for 循环

func main() {
    for i := 0; i < 10; i++ {
        fmt.Println(i)
        if i == 5 {
            goto end
        }
    }
    end:
        fmt.Println("end")
}

输出:

0
1
2
3
4
5
end

switch

第一种语法格式。

func main() {
    num := 1
    switch num {
        case 1:
        	fmt.Println("num=1")
        case 2:
        	fmt.Println("num=2")
        case 3:
        	fmt.Println("num=3")
        default:
        	fmt.Println("未匹配")
    }
}

第二种语法格式。

func main() {
    num := 1
    switch {
        case num == 1:
        	fmt.Println("num=1")
        case num == 2:
        	fmt.Println("num=2")
        case num == 3:
        	fmt.Println("num=3")
        default:
        	fmt.Println("未匹配")
    }
}

第三种语法格式。

func main() {
    switch num := 1; {
        case num == 1:
        	fmt.Println("num=1")
        case num == 2:
        	fmt.Println("num=2")
        case num == 3:
        	fmt.Println("num=3")
        default:
        	fmt.Println("未匹配")
    }
}

第四种语法格式。

使用关键字 fallthrough 。默认在switch中,每个case都会有一个隐藏的break,如果想要去掉隐藏的break,我们就可以使用fallthrough来进行取代。

package main

import (
    "fmt"
)

func main() {
    a := 2
    switch a {
    case 1:
        fmt.Println("a=1")
    case 2:
        fmt.Println("a=2")
        fallthrough
    case 3:
        fmt.Println("a=3")
        case 4:
        fmt.Println("a=4")
    default:
        fmt.Println("default")
    }
}

输出:

a=2
a=3

select

select语句用来处理与channel有关的I/O操作:

  1. 每个case都必须是一个通信;
  2. 所有channel表达式和被发送的表达式都会被求值;
  3. 任意某个通道可以运行,它就执行,其他被忽略;
  4. 多个case可以运行,随机选一个执行;
  5. 都不可以运行,有default,执行default,没有就阻塞,直到某个通信可以运行,且不会重新对表达式求值;
  6. 一个select最多执行一次case里的代码,需要一直检测case,外层加for循环;
  7. case里的break只退出当前select,和for循环无关;

随机执行case用法

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        time.Sleep(time.Second)
        ch1 <- 1
    }()

    go func() {
        time.Sleep(time.Second)
        ch2 <- 2
    }()

    time.Sleep(2 * time.Second)
    select {
    case i := <-ch1:
        fmt.Println("ch1 receive", i)
    case i := <-ch2:
        fmt.Println("ch2 receive", i)
    default:
        fmt.Println("no i/o opeartion")
    }
}

结果随机打印:ch1 receive: 1 或 ch2 receive: 2,因为两个channal等待写入,select里两个case都符合执行条件,随机执行。

设置接收channel超时用法

func main() {
    ch1 := make(chan int)
    select {
    case i := <-ch1:
        fmt.Println(i)
    case <-time.After(5 * time.Second):
        fmt.Println("ch receive timeout")
    }
}

检查 channel 是否满了用法

func main() {
    ch1 := make(chan int, 5)
    ch1 <- 1
    ch1 <- 2
    ch1 <- 3
    ch1 <- 4
    ch1 <- 5

    select {
    case ch1 <- 6:
        fmt.Println("send 6 to ch1")
    default:
        fmt.Println("channel is full")
    }
}

interface

介绍

在Go语言中接口 (interface) 是一种类型, 一种抽象的类型。

接口 (interface) 定义了一个对象的行为规范, 只定义规范不实现,由具体的对象来实现规范的细节。

接口做的事情就像是定义一个协议(规则)。

Interface 是一组method的集合, 是duck-type programming 的一种体现。

接口的定义

  • 接口是一个或多个方法签名的集合
  • 接口只有方法声明,没有实现,没有数据字段
  • 接口可以匿名嵌入其他接口,或嵌入到结构中
  • 接口调用不会做receiver的自动转换
  • 接口同样支持匿名字段方法
  • 接口也可实现类似OOP中的多态
  • 任何类型的方法集中只要拥有该接口'对应的全部方法'签名
  • 只有当接口存储的类型和对象都为nil时,接口才等于nil
  • 用 interface{} 传递任意类型数据是Go语言的惯例用法,而且 interface{} 是类型安全的
  • 空接口可以作为任何类型数据的容器
  • 一个类型可实现多个接口
  • 接口命名习惯以 er 结尾

使用

每个接口由数个方法组成,接口的定义如下:

type 接口类型 interface {
  方法名1 (参数列表1) 返回值列表1
  方法名2 (参数列表2) 返回值列表2
  ...
}

注意

  1. 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  2. 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  3. 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

例子:

type writer interface {
  Write([]byte) error
}

值接收者和指针接收接口

type Mover interface {
  move()
}

type dog struct {}

func (d dog) move() {
  fmt.Println("狗狗")
}

func main() {
  var x Mover
  var wangcai = dog{}
  x = wangcai						// x 可以接收dog类型
  var fugui = &dog{}                // fugui是 *dog 类型 
  x = fugui							// x可以接收*dog类型 指针接收
  x.move()							
}

多个类型实现同一接口

// Mover 接口
type Mover interface {
  move()
}

type dog struct {
  name string
}
type car struct {
  brand string
}

// dog 类型实现 Mover 接口
func (d dog) move() {
  fmt.Printf("%s: mmmm", d.name)
}
// car 类型实现 Mover 接口
func (c car) move() {
  fmt.Printf("%s: mmmm", c.brand)
}

func main() {
  var x Mover
  var a = dog{name: "旺财"}
  var b = car{brand: "虾米"}
  x = a
  x.move()
  x = b
  x.move()
}

一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

接口嵌套

接口与接口间可以通过嵌套创造出新的接口。

type Sayer interface {
    say()
}
type Mover interface {
    move()
}

// 接口嵌套
type animal interface {
    Sayer
    Mover
}

// 嵌套得到的接口的使用与普通接口一样
type cat struct {
    name string
}

func (c cat) say() {
    fmt.Println("ssss")
}

func (c cat) move() {
    fmt.Println("mmmm")
}

func main() {
    var x animal
    x = cat{name: "花花"}
    x.move()
    x.say()
}

空接口

空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口。

空接口类型的变量可以存储任意类型的变量。

func main() {
    // 定义一个空接口 x
    var x interface{}
    s := "test data"
    x = s
    fmt.Printf("type:%T value: %v\n", x, x)
    i := 100
    x = i
    fmt.Printf("type:%T value: %v\n", x, x)
    b := true
    x = b
    fmt.Printf("type:%T value: %v\n", x, x)
}

空接口作为函数的参数

使用空接口实现可以接收任意类型的函数对象。

func show(a interface{}){
    fmt.Printf("type:%T value: %v\n", a, a)
}

空接口作为map的参数

使用空接口实现可以保存任意值的字典。

var Info = make(map[string]interface{})
Info["id"] = 1
Info["name"] = "Alice"
fmt.Println(Info)

获取空接口值

判断空接口中值,可以使用类型断言,语法如下:

x.(T)

x 表示类型为 interface{} 的变量

T 表示断言 x 可能是的类型

该语法返回两个参数,第一个参数是 x 转化为 T 类型后的变量, 第二个值是一个布尔值, 若为 true 则表示断言成功, false 则表示失败。

func main() {
    var x interface{}
    x = "data"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

如果要断言多次,可以写 if 判断, 也可以用 switch 语句实现。

反射

介绍

什么是反射

例如:有时候我们需要知道某个值是什么类型,才能用对等逻辑去处理它。

以下是常用的处理方法:

// 伪代码
switch value := value.(type){
    case string:
    	// 处理操作
    case int:
    	// 处理操作
    ...
}

这样处理,会写的非常长,而且还可能存在自定的类型,也就是说这个判断日后可能还要一直改,因为无法知道未知值到底属于什么类型。

如果使用反射来处理,使用标准库 reflect 中的 TypeOf 和 ValueOf 函数从接口中获取目标对象的信息,就可以轻松处理这个问题。

更多介绍,可参考reflect 官方地址

pkg.go.dev/reflect

Go语言提供了一种机制,在编译时不知道类型的情况下,可更新变量、运行时查看值调用方法以及直接对他们的布局进行操作的机制,称为反射。

使用

使用反射查看对象类型

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var name string = "Alice"
    nameType := reflect.TypeOf(name)
    nameValue := reflect.ValueOf(name)

    fmt.Println("name type: ", nameType)
    fmt.Println("name value: ", nameValue)
}

输出:

name type:  string
name value:  Alice

struct 类型反射用法

package main

import (
	"fmt"
	"reflect"
)

type Info struct {
	Name string
	Desc string
}

func (i Info) Detail() {
	fmt.Println("detail info")
}

func main() {
	i := Info{Name: "Alice", Desc: "技术分享"}
	
	t := reflect.TypeOf(i) // 获取目标对象
	v := reflect.ValueOf(i) // 获取value值
	for i := 0; i < v.NumField(); i++ { // NumField()获取字段总数
		key := t.Field(i) // 根据下标,获取包含的key
		value := v.Field(i).Interface() // 获取key对应的值
		fmt.Printf("key=%s value=%v type=%v\n", key.Name, value, key.Type)
	}

	// 获取Info的方法
	for i := 0; i < t.NumMethod(); i++ {
		m := t.Method(i)
		fmt.Printf("方法 Name=%s Type=%v\n", m.Name, m.Type)
	}
}

输出:

key=Name value=Alice type=string
key=Desc value=技术分享 type=string
方法 Name=Detail Type=func(main.Info)

通过反射判断类型用法

package main

import (
	"fmt"
	"reflect"
)

type Info struct {
	Name string
	Desc string
}

func main() {
	i := Info{Name: "帽儿山的枪手", Desc: "技术分享"}
	t := reflect.TypeOf(i)

	// Kind()函数判断值的类型
	if k := t.Kind(); k == reflect.Struct {
		fmt.Println("struct type")
	}
	num := 100
	switch v := reflect.ValueOf(num); v.Kind() {
	case reflect.String:
		fmt.Println("string type")
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		fmt.Println("int type")
	default:
		fmt.Printf("unhandled kind %s", v.Kind())
	}
}

输出:

struct type
int type

通过反射修改内容

package main

import (
	"fmt"
	"reflect"
)

type Info struct {
	Name string
	Desc string
}

func main() {
	i := &Info{Name: "Alice", Desc: "技术分享"}
	v := reflect.ValueOf(i)

	// 修改值必须是指针类型
	if v.Kind() != reflect.Ptr {
		fmt.Println("不是指针类型")
		return 
	}
	v = v.Elem() // 获取指针指向的元素
	name := v.FieldByName("Desc") // 获取目标key的值
	name.SetString("好好工作")
	fmt.Printf("修改后数据: %v\n", *i)
}

输出:

修改后数据: {Alice 好好工作}

通过反射调用方法

package main

import (
	"fmt"
	"reflect"
)

type Info struct {
	Name string
	Desc string
}

func (i Info) Detail() {
	fmt.Println("detail info")
}

func main() {
	i := Info{Name: "Alice", Desc: "技术分享"}
	v := reflect.ValueOf(i)

	// 获取方法控制权
	mv := v.MethodByName("Detail")
	mv.Call([]reflect.Value{}) // 这里是无调用参数 []reflect.Value{}
}

输出:

detail info

泛型

介绍

泛型的概念,可以从多态看起,多态是同一形式表现出不同行为的一种特性,在编程语言中被分为两类,临时性多态和参数化多态。

根据实参生成不同的版本,支持任意数量的调用,即泛型,简言之,就是把元素类型变成了参数。

golang版本需要在 1.17版本或以上,才支持泛型使用。

(1.17版本泛型是golang推出的尝鲜版,1.18是正式版本)

举例:

func Add(a, b int) int{}
func AddFloat(a, b float64) float64{}

在泛型的帮助下,上面代码就可以简化成为:

func Add[T any](a, b T) T

Add后面的[T any],T表示类型的标识,any表示T可以是任意类型。

a、b和返回值的类型T和前面的T是同一个类型。

为什么用[],而不是其他语言中的<>,官方有过解释,大概就是<>会有歧义。曾经计划使用() ,因为太容易混淆,最后使用了[]。

泛型三大概念

  • 类型参数
  • 类型约束
  • 类型推导

特性

  • 函数可以通过type关键字引入额外的类型参数(type parameters)列表:func F(type T)(p T) { ... }
  • 这些类型参数可以像一般的参数一样在函数体中使用
  • 类型也可以拥有类型参数列表:type M(type T) []T
  • 每个类型参数可以拥有一个约束:func F(type T Constraint)(p T) { ... }
  • 使用interface来描述类型的约束
  • 被用作类型约束的interface可以拥有一个预声明类型列表,限制了实现此接口的类型的基础类型
  • 使用泛型函数或类型时需要传入类型实参
  • 类型推断允许用户在调用泛型函数时省略类型实参
  • 泛型函数只允许进行类型约束所规定的操作

使用

对泛型进行输出

如果Go当前版本是1.17版本,运行时需要加参数 -gcflags=-G=3

# 完整命令
go run -gcflags=-G=3 example.go 

示例:

package main

import (
	"fmt"
)

func print[T any](s []T) {
	for _, v := range s {
		fmt.Printf("%v ", v)
	}
	fmt.Printf("\n")
}

func main() {
	print[int]([]int{1,2,3,4})
	print[float64]([]float64{1.01, 2.02, 3.03, 4.04})
	print[string]([]string{"a", "b", "c", "d"})
}

输出:

1 2 3 4 
1.01 2.02 3.03 4.04 
a b c d 

Go1.18 中,any 是 interface{} 的别名。

使用泛型约束控制类型的使用范围

原先的语法中,类型约束会用逗号分隔的方式来展示。

type int, int8, int16, int32, int64

在新语法中,结合定义为 union element(联合元素),写成一系列由竖线 ”|“ 分隔的类型或近似元素。

int | int8 | int16 | int32 | int64

示例:

package main

import (
	"fmt"
)

type CustomType interface {
	int | int8 | int16 | int32 | int64 | string
}

func add[T CustomType] (a, b T) T{
	return a + b
}

func main() {
	fmt.Println(add(1, 2))
	fmt.Println(add("Alice", "技术分享"))
}

输出:

3 Alice技术分享

上述 CustomType 接口类型也可以写成以下格式:

type CustomType interface {
	~int | ~string
}

上述声明的类型集是 ~int,也就是所有类型为 int 的类型(如:int、int8、int16、int32、int64)都能够满足这个类型约束的条件。

泛型中自带 comparable 约束

因为不是所有的类型都可以==比较,所以Golang内置提供了一个comparable约束,表示可比较的。

官方说明

comparable是由所有可比较类型(布尔、数字、字符串、指针、通道、可比较类型的数组、字段均为可比较类型的结构)实现的接口。可比较接口只能用作类型参数约束,不能用作变量的类型。

pkg.go.dev/builtin@mas…

package main

import (
	"fmt"
)

func diff[T comparable](a []T, v T) {
	for _, e := range a {
		if e == v {
			fmt.Println(e)
		}
	}
}

func main() {
	diff([]int{1, 2, 3, 4}, 3)
}

输出:

3

泛型中操作指针

package main

import (
	"fmt"
)

func pointerOf[T any](v T) *T {
	return &v
}

func main() {
	name := pointerOf("Alice")
	fmt.Println(*name)
	id := pointerOf(100)
	fmt.Println(*id)
}

输出:

Alice
100