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

70 阅读11分钟

出现原因:需要一款既有高效的执行速度,又有较快的编译速度和开发速度

变量

  • 单个变量的声明语法:var 变量名 变量类型

  • 多个变量的声明语法:

    var (
    	变量1 类型
        变量2 类型
    )
    
  • 自动推导写法(只能用在函数内部,且必须显式初始化):变量名 := 初始化值

  • 变量交换:a,b = b,a

  • 匿名变量:用下划线_表示匿名变量,相当于一个占位符,任何赋给它的值都会被抛弃

    适用情景:函数有多个返回值,但是程序只需要其中某个或某些返回值,其他返回值就能用匿名变量接收

    下划线_在go语言中的作用:

    • import中,import _ 包名可以仅仅调用该包中的init()函数,而不导入该包中的其他函数
    • 匿名变量

常量

  • 常量的数据类型只能是布尔型、数字型(整数、浮点数、复数)和字符串

  • 语法:const 常量名 [类型],类型可以不写(通过初始化的类型自动推导得到常量类型)

  • iota:一个特殊常量(常量计数器)

    • 每次遇到const关键字,iota就重置为0

    • iota可以理解为在const语句块内部的常量索引

    • 常量语句块中,如果某个常量没有初始值,则默认与上行一致,于是可以有如下简写:

      const (
      	a = iota
          b
          c
      )
      //b和c默认都为iota,又由于iota是语句块内部的常量索引,所以a=0, b=1, c=2
      

输出

  • fmt.Printf的格式化占位符
    • %p:打印内存地址
    • %T:打印变量类型
    • %t:打印布尔类型
    • %v:相应值的默认格式
    • %+v:打印结构体时,会添加字段名
    • %#v:相应的go语法表示

数据类型

数字类型

  • 整型:uint8uint16uint32uint64int8int16int32int64
  • 浮点型:float32float64
  • 复数:complex64complex128

字符与字符串

  • 字符用单引号表示,实际上是int32类型
  • 字符串用双引号表示
  • 字符串可以用+连接

数据类型转换

go语言不存在隐式类型转换,必须用显式类型转换,语法为:数据类型(变量)

数组(Array)

数组是值类型,而不是引用类型

  • 定义

    var arr [5]int = [5]int{1,2,3} //没初始化的默认为0
    var arr = [5]int{1,2,3} //当然也可以自动推导
    arr := [5]int{1,2,3} //局部范围内还可以更加简化
    
  • 初始化(以局部变量为例)

    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
    

切片(Slice)

切片是基于一个底层数组的。基于同一个数组的任何切片,都是在同一个内存地址上进行访问和修改的

slice是结构体包装的,本质上是一个结构体{ pointer, len, cap }

指针

Go语言中的指针不能进行偏移和运算,是安全指针。

Map

  • 定义:map[KeyType]ValueType
  • 判断某个key是否存在:value, ok := map[key]
  • map是由指针包装的

结构体

Go语言中通过结构体来实现面向对象

类型定义和类型别名

//类型定义
type NewInt int //编译完成后,该类型还会一直存在
//类型别名
type MyInt = int //MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型

结构体初始化

  • 使用键值对初始化(最后一行记得也要加逗号)
  • 使用值列表初始化

注意:

  1. 使用值列表初始化时,必须初始化所有字段,且要按声明的顺序初始化
  2. 两种初始化方式不能混用

调用结构体的字段

不论是结构体的值还是指针,都可以用.取某个字段,这是go语言的一个语法糖

方法

  • 接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法
  • 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法

匿名字段

  • 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段

  • 匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

  • 结构体内可以嵌套另一个结构体,且可以是匿名的。如果想访问嵌套的匿名结构体内的字段,有两种方法:

    • 匿名结构体类型名.字段名
    • 直接访问匿名结构体的字段名(原理:当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找)

    嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段

“继承”

通过嵌套匿名结构体(这样就可以通过直接访问字段名的方式访问被嵌套的结构体的字段)实现继承

可见性

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

结构体标签(Tag)

  • Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来

  • Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下

    `key1:"value1" key2:"value2"`  
    

内存分配

  • new func new(Type) *Type

    new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

  • makefunc make(t Type, size ...IntegerType) Type

    make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身

运算符

位运算符

  • &^:位清空。对于a &^ b,每个对应位上,b为0,则取a的值;b为1,则结果为0

流程控制

条件语句if

  • go语言不支持三元操作符a > b ? a : b
  • 注意大括号、else ifelse的位置,不能换行
  • if语句后可以跟初始化语句,定义代码块局部变量

条件语句switch

  • Go语言的switch分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止

  • 如果switch关键字后没有表达式,它会匹配true

  • 当匹配成功后,默认自动终止,但也可以使用fallthrough强制执行后面的case代码

  • switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型

    switch x.(type){
        case type:
           statement(s)      
        case type:
           statement(s)
        /* 你可以定义任意个数的case */
        default: /* 可选 */
           statement(s)
    }   
    

条件语句select

  • 每个case都必须是一个通信

  • select会随机执行一个可运行的case

  • 如果没有default语句,且没有case可运行,它将阻塞,直到有case可运行

  • 如果有default语句,则会在没有通信可执行的时候执行default的代码

循环语句for

1. 分号形式

跟一般的for循环用法基本一致

2. condition形式

for condition {
    
}

这种形式与一般的while循环类似,如果condition不写,则默认为true

3. range形式

  • 类似迭代器操作,返回 (索引, 值) 或 (键, 值)
  • for 循环的 range 格式可以对 slice、map、数组、字符串、channel等进行迭代循环
  • range 会复制对象,即range是在副本中进行遍历的(对于slice这种引用类型,复制的是struct slice { pointer, len, cap }

循环控制

  • gotocontinuebreak
  • 三个控制语句都能配合标签(label)使用
  • 标签格式为标签名:,用于标记某个循环体。与控制语句配合使用,可以做到跳出多层循环

函数

  • 无需声明原型
  • 支持不定变参
  • 支持多返回值
  • 支持命名返回参数
  • 支持匿名函数和闭包
  • 函数也是一种类型,一个函数可以赋值给变量
  • 不支持嵌套 (nested) 一个包不能有两个名字一样的函数
  • 不支持重载 (overload)
  • 不支持默认参数 (default parameter)

可变参数

  • 可变参数本质上就是 slice。只能有一个,且必须是最后一个
  • 格式:args ...类型名称
  • 使用 slice 对象做变参时,必须展开。slice...

返回值

  • 返回值可以被命名,并且就像在函数体开头声明的变量那样使用

  • 命名返回参数可看做与形参类似的局部变量,最后由 return 隐式返回。但如果被同名局部变量遮蔽,就需要显式返回。

  • 显式 return 返回前,会先修改命名返回参数,例如:

    func add(x, y int) (z int) {
        defer func() {
            println(z)
        }()
    
        z = x + y
        return z + 200 // 执行顺序: (z = z + 200) -> (call defer) -> (return)
    }
    

闭包

  • 闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)
  • 闭包(Closure),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外

延迟调用(defer)

1. 关键字 defer 用于注册延迟调用。
2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
3. 多个defer语句,按先进后出的方式执行。
4. defer语句中的变量,在defer声明时就决定了。
  • 当defer后面的语句是一个闭包,在return前会调用这个闭包,并且闭包中的变量值为当前值,而不是定义defer时的值
  • defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。但是对于方法中的this指针,go语言并不会复制一份,而是会使用return前那个时刻this指针指向的对象

异常处理

  • defer会在两种情况下执行:1. 函数正常执行完毕后,返回前;2. 发生(抛出)异常时
  • 延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获
  • 可以用panic()抛出异常,也可以用errors.New()定义一个error并通过函数返回值返回

方法

定义

func (recevier type) methodName(参数列表)(返回值列表){}
//参数和返回值可以省略 

表达式

  • method value:绑定实例,隐式传参
  • method expression:必须显式传参
package main

import "fmt"

type User struct {
    id   int
    name string
}

func (self *User) Test() {
    fmt.Println(self)
}

func main() {
    u := User{1, "Tom"}

    mValue := u.Test
    mValue() // 隐式传递 receiver

    mExpression := (*User).Test
    mExpression(&u) // 显式传递 receiver
}  
  • 需要注意,method value 会复制 receiver。
package main

import "fmt"

type User struct {
    id   int
    name string
}

func (self User) Test() {
    fmt.Println(self)
}

func main() {
    u := User{1, "Tom"}
    mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。

    u.id, u.name = 2, "Jack"
    u.Test()

    mValue()
} 
//输出结果:
//{2 Jack}
//{1 Tom}  

接口

接口是一种类型

定义

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

值接收者和指针接收者实现接口的区别

  • 使用值接收者实现接口之后,不管是结构体还是结构体指针类型的变量都可以赋值给接口变量。
  • 使用指针接收者实现接口之后,只能将结构体指针类型的变量赋值给接口变量

注意事项

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

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

  • 空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。空接口类型的变量可以存储任意类型的变量。

    空接口的作用:

    • 接收任意类型的函数参数
    • 可以保存任意值的字典
  • 类型断言

    x.(T)
    //x:表示类型为interface{}的变量
    //T:表示断言x可能是的类型。