【青训营】Golang 基础 | 青训营笔记

270 阅读33分钟

Golang语法

这是我参与「第三届青训营 -后端场」笔记创作活动的的第1篇笔记。

Go的学习网上有很多详细的博客文章,推荐:

Go基础

Go语言的主要特征

Go语言的主要特征

思想:Less can be more 大道至简,小而蕴真 让事情变得复杂很容易,让事情变得简单才难 深刻的工程文化。

Go优点:

  • 自带gc;
  • 静态编译,编译好后,扔服务器直接运行;
  • 简单的思想,没有继承,多态,类等;
  • 丰富的库和详细的开发文档;
  • 语法层支持并发,和拥有同步并发的channel类型,使并发开发变得非常方便;
  • 简洁的语法,提高开发效率,同时提高代码的阅读性和可维护性;
  • 超级简单的交叉编译,仅需更改环境变量。

Go主要特征:

  1. 自动立即回收
  2. 更丰富的内置类型;
  3. 函数多返回值
  4. 错误处理;
  5. 匿名函数和闭包;
  6. 类型和接口;
  7. 并发编程
  8. 反射;
  9. 语言交互性。

golang文件命名:所有的go源码都是以 .go结尾。

Go语言命名

Go的函数、变量、常量、自定义类型、包(package)的命名方式遵循以下规则:

  1. 首字符可以是任意的Unicode字符或者下划线
  2. 剩余字符可以是Unicode字符、下划线、数字
  3. 字符长度不限

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

Go还有37个保留字:

  • Constants:

     true  false  iota  nil
    
  • Types:

     int  int8  int16  int32  int64  
     uint  uint8  uint16  uint32  uint64  uintptr
     float32  float64  complex128  complex64
     bool  byte  rune  string  error
    
  • Functions:

     make  len  cap  new  append  copy  close  delete
     complex  real  imag
     panic  recover
    

可见性:

  1. 声明在函数内部,是函数的本地值,类似private;
  2. 声明在函数外部,是对当前包可见(包内所有.go文件都可见)的全局值,类似protect;
  3. 声明在函数外部且首字母大写是所有包可见的全局值,类似public。

Go语言的四种主要声明方式:

  • var(声明变量)
  • const(声明常量)
  • type(声明类型)
  • func(声明函数)

最常用的,还是:=的使用。可以直接定义没有声明过的变量,且无需我们自己指定类型。例如:

 package main
 ​
 import "fmt"
 ​
 func main(){
    num := 1011
    c := 'x'
    str := "hello"
    fmt.Println(num)
    fmt.Printf("%c\n", c)
    fmt.Println(str)
 }

Golang内置类型和内置函数

Golang内置类型和函数

值类型和引用类型

  • 值类型:

    1. bool
    2. int(32 or 64), int8, int16, int32, int64
    3. uint(32 or 64), uint8(byte), uint16, uint32, uint64
    4. float32, float64
    5. string
    6. complex64, complex128(复数)
    7. array (固定长度的数组)
  • 引用类型:

    1. slice(切片,或者叫序列数组)(最常用)
    2. map(映射)
    3. chan(管道)

例子:

 package main
 ​
 import "fmt"
 ​
 func main(){
    var isStudent bool
    isStudent = true
    fmt.Println(isStudent)
 }

内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:lencapappend,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

  • append:用来追加元素到slice(或数组)中,返回修改后的slice(或数组)
  • close:主要用来关闭channel
  • delete:从map中删除key对应的value
  • panic:停止常规的goroutine (panic和recover:用来做错误处理)
  • recove:允许程序定义goroutine的panic动作
  • real:返回complex的实部 (complex、real imag:用于创建和操作复数)
  • imag:返回complex的虚部
  • make:用来分配内存,返回Type本身(只能应用于slice, map, channel)
  • new:用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
  • cap:capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
  • copy:用于复制和连接slice,返回复制的数目
  • len:来求长度,比如string、array、slice、map、channel ,返回长度
  • printprintln:底层打印函数,在部署环境中建议使用 fmt 包

内置接口error:

返回值为String的都实现了error接口

     type error interface {
             Error()    String
     }

Init函数和main函数

Init函数和main函数

init函数:

go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。

特征如下:

  1. init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
  2. 每个包可以拥有多个init函数
  3. 包的每个源文件也可以拥有多个init函数
  4. 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
  5. 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
  6. init函数不能被其他函数调用,而是在main函数执行之前,自动被调用

main函数:

Go语言程序的默认入口函数(主函数):func main()。函数体用{}一对括号包裹。

 func main(){
         //函数体
     }

init函数和main函数的异同:

  • 相同点:

    • 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
  • 不同点:

    • init可以应用于任意包中,且可以重复定义多个;
    • main函数只能用于main包中,且只能定义一个。

两个函数的执行顺序:

  • 对同一个go文件的init()调用顺序是从上到下的;
  • 对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数;
  • 对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
  • 如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。

Go相关命令

命令

安装好golang环境后,有如下命令:

  • go env:打印Go语言的环境信息
  • go run:编译并运行命令源码文件
  • go get:根据要求和实际情况从互联网上下载或更新指定的代码包及其依赖包,并对它们进行编译和安装
  • go build:编译我们指定的源码文件或代码包以及它们的依赖包
  • go install:用于编译并安装指定的代码包及它们的依赖包
  • go clean:删除掉执行其它命令时产生的一些文件和目录
  • go doc:打印附于Go语言程序实体上的文档。我们可以通过把程序实体的标识符作为该命令的参数来达到查看其文档的目的
  • go test:对Go语言编写的程序进行测试
  • go list:列出指定的代码包的信息
  • go fix:把指定代码包的所有Go语言源码文件中的旧版本代码修正为新版本的代码
  • go vet:用于检查Go语言源码中静态错误的简单工具
  • go tool pprof:交互式地访问概要文件的内容

Go运算符

运算符

Go 语言内置的运算符有:

  • 算术运算符

    运算符描述
    +相加
    -相减
    *相乘
    /相除
    %求余

    注意: ++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。

  • 关系运算符

    运算符描述
    ==检查两个值是否相等,如果相等返回 True 否则返回 False。
    !=检查两个值是否不相等,如果不相等返回 True 否则返回 False。
    检查左边值是否大于右边值,如果是返回 True 否则返回 False。
    >=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。
    <检查左边值是否小于右边值,如果是返回 True 否则返回 False。
    <=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。
  • 逻辑运算符

    &&逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。
    ll逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False。
    !逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。
  • 位运算符

    位运算符对整数在内存中的二进制位进行操作。

    运算符描述
    &参与运算的两数各对应的二进位相与。(两位均为1才为1)
    l参与运算的两数各对应的二进位相或。(两位有一个为1就为1)
    参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(两位不一样则为1)
    <<左移n位就是乘以2的n次方。“a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。
    >>右移n位就是除以2的n次方。“a>>b”是把a的各二进位全部右移b位。
  • 赋值运算符

    运算符描述
    =简单的赋值运算符,将一个表达式的值赋给一个左值
    +=相加后再赋值
    -=相减后再赋值
    *=相乘后再赋值
    /=相除后再赋值
    %=求余后再赋值
    <<=左移后赋值
    >>=右移后赋值
    &=按位与后赋值
    l=按位或后赋值
    ^=按位异或后赋值

整体来说运算符和Java相比并没有什么区别。

给个例子:

 package main
 ​
 import "fmt"
 ​
 func main(){
    var score1 float64
    var score2 float64
    fmt.Print("请输入平时平时成绩:")
    fmt.Scan(&score1)
    fmt.Print("请输入平时卷面成绩:")
    fmt.Scan(&score2)
    if score1*0.4+score2*0.6 >=60 {
       fmt.Println("合格!")
    }else {
       fmt.Println("不合格!")
    }
 }

Go下划线_

下划线

在Golang里,_是特殊标识符,用来忽略结果。

下划线在import中

在Golang里,import的作用是导入其他package。

import _(如:import hello/imp)的作用:当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import 引用该包。即使用import _ 包路径仅仅是为了调用该包的init()函数,无法通过包名来调用包中的其他函数。

下划线在代码中

看个例子:

 package main
 ​
 import (
     "os"
 )
 ​
 func main() {
     buf := make([]byte, 1024)
     f, _ := os.Open("/Users/***/Desktop/text.txt")
     defer f.Close()
     for {
         n, _ := f.Read(buf)
         if n == 0 {
             break    
 ​
         }
         os.Stdout.Write(buf[:n])
     }
 }
  • 解释1:下划线_意思是忽略这个变量。比如os.Open,返回值为*os.File,error。普通写法是f,err := os.Open("xxxxxxx")。如果此时不需要知道返回的错误值,就可以用f, _ := os.Open("xxxxxx"),如此则忽略了error变量。
  • 解释2:_表示占位符,意思是那个位置本应赋给某个值,但是我们不需要这个值,所以就把该值赋给下划线,意思是丢掉不要。这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。这种情况是占位用的,方法返回两个结果,而你只想要一个结果,那另一个就用_占位;而如果用变量的话,不使用,编译器是会报错的。

补充:

 import "database/sql"
 import _ "github.com/go-sql-driver/mysql"

第二个import就是不直接使用mysql包,只是执行一下这个包的init函数,把mysql的驱动注册到sql包里,然后程序里就可以使用sql包来访问mysql数据库了。

Go变量和常量

变量和常量

变量

变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一常见变量的数据类型有:整型、浮点型、布尔型等。

Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。同一作用域内不支持重复声明,并且Go语言的变量声明后必须使用

所以接下来看一下Go中变量的声明。

  • 标准声明:

         var 变量名 变量类型
         var name string
         var age int
         var isOk bool
    
  • 批量变量:

         var (
             a string
             b int
             c bool
             d float32
         )
    
  • 短变量声明:在函数内部,可以使用更简略的 := 方式声明并初始化变量

     package main
     ​
     import "fmt"
     ​
     // 全局变量m
     var m = 100
     ​
     func main() {
        n := 10
        m := 200 // 此处声明局部变量m
        fmt.Println(m, n)
     }
    

    结果为:200 10

然后看一下变量初始化。

Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

     var 变量名 类型 = 表达式
     var name string = "pprof.cn"
     var sex int = 1
     // 有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
     var name = "pprof.cn"
     var sex = 1
     // 一次初始化多个变量
     var name, sex = "pprof.cn", 1

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

 package main
 ​
 import "fmt"
 ​
 func main() {
    var x int
    _, err := fmt.Scan(&x)
    if err!=nil {
       fmt.Println(x)
    }
 }

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)

注意:

  • 函数外的每个语句都必须以关键字开始(varconstfunc等)
  • :=不能使用在函数外!(所以全局变量不能使用:=
  • _多用于占位,表示忽略值

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var换成了const常量在定义的时候必须赋值。

     const pi = 3.1415
     const e = 2.7182

声明了pie这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。

多个常量也可以一起声明:

     const (
         pi = 3.1415
         e = 2.7182
     )

const同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:

     const (
         n1 = 100
         n2
         n3
     )

上面示例中,常量n1、n2、n3的值都是100

iota

iota是在Java中没有的。iotago语言的常量计数器,只能在常量的表达式中使用iotaconst关键字出现时将被重置为0const每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。

使用iota能简化定义,在定义枚举时很有用。

举个例子:

    const (
            n1 = iota //0
            n2        //1
            n3        //2
            n4        //3
        )

几个常见的iota示例:

  • 使用_跳过某些值:

         const (
                 n1 = iota //0
                 n2        //1
                 _
                 n4        //3
             )
    
  • iota声明中间插队:

     const (
             n1 = iota //0
             n2 = 100  //100
             n3 = iota //2
             n4        //3
         )
     const n5 = iota //0
    
  • 定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)

     const (
             _  = iota
             KB = 1 << (10 * iota)
             MB = 1 << (10 * iota)
             GB = 1 << (10 * iota)
             TB = 1 << (10 * iota)
             PB = 1 << (10 * iota)
         )
    
  • 多个iota定义在一行:

    const (
            a, b = iota + 1, iota + 2 //1,2(ioat=0)
            c, d                      //2,3(ioat=1)
            e, f                      //3,4(ioat=2)
        )
    

Go基本类型

基本类型

Golang更明确数字类型的命名,支持 Unicode,支持常用数据结构。

类型长度(字节)默认值说明
bool1false
byte10uint8(ASCII码字符
rune40Unicode Code Point, int32(UTF-8字符
int, uint4或8032 或 64 位
int8, uint810-128 ~ 127, 0 ~ 255,byte是uint8 的别名
int16, uint1620-32768 ~ 32767, 0 ~ 65535
int32, uint3240-21亿~ 21亿, 0 ~ 42亿,rune是int32 的别名
int64, uint6480-9223372036854775808 ~ 9223372036854775807,0 ~ 18446744073709551615
float3240.0
float6480.0
complex648
complex12816
uintptr4或8以存储指针的 uint32 或 uint64 整数
array值类型
struct值类型
string""UTF-8 字符串
slicenil引用类型
mapnil引用类型
channelnil引用类型
interfacenil接口
functionnil函数

支持八进制、 六进制,以及科学记数法。标准库 math 定义了各数字类型取值范围。例如:

     a, b, c, d := 071, 0x1F, 1e9, math.MinInt16

空指针值 nil,而非Java的 null。

整型

整型分为以下两个大类:

  • 按长度分为:int8int16int32int64
  • 对应的无符号整型:uint8uint16uint32uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

浮点型

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准:

  • float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32
  • float64 的浮点数的最大范围约为 1.8e308,可以使用常量定义:math.MaxFloat64

复数

  • complex64的实部和虚部为32位
  • complex128的实部和虚部为64位

布尔值

Go语言中以bool类型进行声明布尔型数据,布尔型数据只有truefalse两个值。

注意:布尔类型变量的默认值为false。Go 语言中不允许将整型强制转换为布尔型,布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型intboolfloat32float64 等一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号""中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:

s1 := "hello"
s2 := "你好"

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示:

转义含义
\r回车符(返回行首)
\n换行符(直接跳到下一行的同列位置)
\t制表符
'单引号'
"双引号"
\反斜杠``

举个例子:

package main

import (
   "fmt"
)

func main() {
      fmt.Println(" "E:\seastar" ")
}

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符(键盘Tab键的上方):

package main

import (
   "fmt"
)

func main() {
   s1 := `
雄关漫道真如铁,
而今迈步从头越。`
   fmt.Println(s1)
}

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

字符串的常用操作

方法介绍
len(str)求长度
+或fmt.Sprintf拼接字符串
strings.Split(s, sep)分割
strings.Contains(s, substr)判断是否包含
strings.HasPrefix(s, suffix)strings.HasSuffix(s, suffix)前缀/后缀判断
strings.Index(s, substr)strings.LastIndex(s, substr)子串出现的位置
strings.Join(a[]string, sep string)join操作

举个例子:

 package main
 ​
 import (
    "fmt"
    "strings"
 )
 ​
 func main() {
    s := "hello"
    fmt.Println(len(s))       // 5
 ​
    split := strings.Split(s, "e")
    fmt.Println(split)    // [h llo]
    for i := range split { // 依次输出h,llo
       fmt.Println(split[i])
    }
 ​
    fmt.Println(strings.Contains(s,"ll"))  // true
    fmt.Println(strings.HasPrefix(s, "he"))    // true
    fmt.Println(strings.HasSuffix(s, "loo"))   // false
 ​
    fmt.Println(strings.Index(s, "l"))    // 2
    fmt.Println(strings.LastIndex(s, "l")) // 3
 ​
    fmt.Println(strings.Join(split,","))      // h,llo
 }

byte和rune类型

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号包裹起来,如:

    var a := '中'
    var b := 'x'

Go 语言的字符有以下两种:

  • uint8类型(或者叫 byte 型):代表一个ASCII码字符
  • rune类型:代表一个UTF-8字符

字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是由byte字节组成,所以字符串的长度是byte字节的长度

rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

强制类型转换的基本语法如下:

T(表达式)

其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等。

举个例子,比如计算直角三角形的斜边长时使用math包的Sqrt()函数,该函数接收的是float64类型的参数,而变量a和b都是int类型的,这个时候就需要将a和b强制类型转换为float64类型:

func sqrtDemo() {
   var a, b = 3, 4
   var c int
   // math.Sqrt()接收的参数是float64类型,需要强制转换
   c = int(math.Sqrt(float64(a*a + b*b)))
   fmt.Println(c)
}

Go的类型别名和自定义类型

自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

    //将MyInt定义为int类型
    type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

    type TypeAlias = Type

之前见过的runebyte就是类型别名,在Go源码中:

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

类型定义和类型别名的区别

类型别名与类型定义表面上看只有一个等号=的差异,可以通过以下代码看一下:

package main

import "fmt"

// 类型定义
type NewInt int

// 类型别名
type MyInt = int

func main() {
   var a NewInt
   var b MyInt

   fmt.Printf("%T\n", a)  // main.NewInt
   fmt.Printf("%T\n", b)  // int
}

数组array

数组Array

Golang的array和以往认知的数组有很大不同。

1.数组是同一种数据类型的固定长度的序列。 数组定义:

var a [len]int

比如:var a [5]int数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。长度是数组类型的一部分,因此,var a[5] int和var a[10]int不同的类型

2.数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是len-1

for i := 0; i < len(a); i++ {
}
for index, v := range a {
}

3.访问越界:如果下标在数组合法范围之外,则触发访问越界,会panic

4.数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。

5.支持 ==!= 操作符,因为内存总是被初始化过的。

package main

import "fmt"

func main() {
   arr1 := [3]int{1,2,3}
   arr2 := [3]int{1,2,3}
   fmt.Println(arr1==arr2)    // true
}

6.注意指针数组 [n]*T和数组指针 *[n]T的区别。

一维数组

一维数组初始化:

 package main
 ​
 import "fmt"
 ​
 // 全局
 var arr0 [5]int = [5]int{1, 2, 3}
 var arr1 = [5]int{1, 2, 3, 4, 5}
 var arr2 = [...]int{1, 2, 3, 4, 5, 6}
 var str = [5]string{3: "hello world", 4: "tom"}
 ​
 func main() {
     // 局部
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
    d := [...]struct {
       name string
       age  uint8
    }{
       {"user1", 10}, // 可省略元素类型。
       {"user2", 20}, // 别忘了最后一行的逗号。
    }
    fmt.Println(arr0, arr1, arr2, str)
    fmt.Println(a, b, c, d)
 }

输出结果如下:

多维数组

二维数组初始化:

 package main
 ​
 import "fmt"
 ​
 // 全局
 var arr0 [5][3]int
 var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
 ​
 func main() {
    // 局部
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}}    // 第 2 纬度不能用 "..."。
    fmt.Println(arr0, arr1)
    fmt.Println(a, b)
 }

注意在二维数组中第 2 纬度不能用 ...

上面已经说到过了,数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值值拷贝行为会造成性能问题,通常会建议使用 slice,或数组指针。

内置函数 lencap 都返回数组长度 (元素数量):

package main

import "fmt"

func main() {
   a := [...][3]int{{1, 2, 3}, {4, 5, 6}}
   fmt.Println(len(a), len(a[0])) // 2 3
   fmt.Println(cap(a), cap(a[0])) // 2 3
}

二维数组遍历:

package main

import "fmt"

func main() {
   arrs := [...][3]int{
      {1,2,3},
      {4,5,6},
      {7,8,9},
   }
   for i, arr := range arrs {
      for j, v := range arr {
         fmt.Printf("(%d,%d)=%d ",i,j,v)
      }
      fmt.Println()  // 换行
   }
}

也可以这么用:

package main

import "fmt"

func main() {
   arrs := [...][3]int{
      {1,2,3},
      {4,5,6},
      {7,8,9},
   }
   for i := range arrs {
      for j := range arrs[i] {
         fmt.Println(arrs[i][j])
      }
   }
}

切片Slice

切片Slice

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

1.切片是数组的一个引用,因此切片是引用类型但自身是结构体,值拷贝传递。

2.切片的长度可以改变,因此,切片是一个可变的数组。

3.切片遍历方式和数组一样,可以用len()求长度,表示可用元素数量,读写操作不能超过该限制。

4.cap()可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。

5.切片的定义:var 变量名 []类型,比如 :

var str []string
var arr []int

6.如果 slice == nil,那么 len、cap 结果都等于 0。

创建切片

首先看一下创建切片的各种方式:

 package main
 ​
 import "fmt"
 ​
 func main() {
    //1.声明切片
    var s1 []int
    if s1 == nil {
       fmt.Println("s1是空")
    } else {
       fmt.Println("s1不是空")
    }
    // 2.:=
    s2 := []int{1,2}
    // 3.make()
    s3 := make([]int, 3)
    fmt.Println(s1, s2, s3)       // [] [1 2] [0 0 0]
    // 4.初始化赋值
    s4 := make([]int, 2, 5)       // [0 0](切片长度len为2,容量cap为5)
    fmt.Println(s4)
    s5 := []int{9,8,7}
    fmt.Println(s5)       // [9 8 7]
    // 5.从数组切片
    arr := [5]int{1, 2, 3, 4, 5}
    s6 := arr[1:4] // 前闭后开
    fmt.Println(s6)    // [2 3 4]
 }

可以看到,用var声明切片和声明数组的区别就在于:数组要在定义的时候就指定长度,而切片不用:

  • 声明数组:var arr [5]int
  • 声明切片:var s1 []int

切片初始化

全局:
var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[start:end] 
var slice1 []int = arr[:end]        
var slice2 []int = arr[start:]        
var slice3 []int = arr[:] 
var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
局部:
arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
slice5 := arr[start:end]
slice6 := arr[:end]        
slice7 := arr[start:]     
slice8 := arr[:]  
slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素

切片大体上跟Python差不多,注意下是前闭后开即可。看个demo就能懂:

package main

import (
   "fmt"
)

var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
var slice0 []int = arr[2:8]
var slice1 []int = arr[0:6]         //可以简写为 var slice []int = arr[:end]
var slice2 = arr[5:10]            //可以简写为 var slice[]int = arr[start:]
var slice3 = arr[0:len(arr)]      //var slice []int = arr[:]
var slice4 = arr[:len(arr)-1]       //去掉切片的最后一个元素
func main() {
   fmt.Printf("全局变量:arr %v\n", arr)
   fmt.Printf("全局变量:slice0 %v\n", slice0)
   fmt.Printf("全局变量:slice1 %v\n", slice1)
   fmt.Printf("全局变量:slice2 %v\n", slice2)
   fmt.Printf("全局变量:slice3 %v\n", slice3)
   fmt.Printf("全局变量:slice4 %v\n", slice4)
   fmt.Printf("-----------------------------------\n")
   arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
   slice5 := arr[2:8]
   slice6 := arr[0:6]         //可以简写为 slice := arr[:end]
   slice7 := arr[5:10]        //可以简写为 slice := arr[start:]
   slice8 := arr[0:len(arr)]  //slice := arr[:]
   slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
   fmt.Printf("局部变量: arr2 %v\n", arr2)
   fmt.Printf("局部变量: slice5 %v\n", slice5)
   fmt.Printf("局部变量: slice6 %v\n", slice6)
   fmt.Printf("局部变量: slice7 %v\n", slice7)
   fmt.Printf("局部变量: slice8 %v\n", slice8)
   fmt.Printf("局部变量: slice9 %v\n", slice9)
}

通过make来创建切片

通过make来创建切片的方式:

  • var slice []type = make([]type, len)
  • slice := make([]type, len)
  • slice := make([]type, len, cap)

看个例子就能懂:

package main

import (
   "fmt"
)

var slice0 []int = make([]int, 5)
var slice1 = make([]int, 5)
var slice2 = make([]int, 5, 10)

func main() {
   fmt.Printf("make全局slice0 :%v\n", slice0)
   fmt.Printf("make全局slice1 :%v\n", slice1)
   fmt.Printf("make全局slice2 :%v\n", slice2)
   fmt.Println("--------------------------------------")
   slice3 := make([]int, 10)
   slice4 := make([]int, 10)
   slice5 := make([]int, 10, 10)
   fmt.Printf("make局部slice3 :%v\n", slice3)
   fmt.Printf("make局部slice4 :%v\n", slice4)
   fmt.Printf("make局部slice5 :%v\n", slice5)
}

切片的内存布局:

读写操作实际目标是底层数组,只需注意索引号的差别。看个例子:

package main

import (
   "fmt"
)

func main() {
   data := [...]int{0, 1, 2, 3, 4, 5}

   s := data[2:4]
   s[0] = s[0]+100
   s[1] = s[1]+200

   fmt.Println(s)    // [102 203]
   fmt.Println(data)  // [0 1 102 203 4 5](注意底层数组也发生改变了)
}

可直接创建 slice 对象,自动分配底层数组:

package main

import (
   "fmt"
)

func main() {
   s1 := []int{0, 1, 2, 3, 8: 100} // 通过初始化表达式构造,可使用索引号。
   fmt.Println(s1, len(s1), cap(s1))  // [0 1 2 3 0 0 0 0 100] 9 9

   s2 := make([]int, 6, 8) // 使用 make 创建,指定 len 和 cap 值。
   fmt.Println(s2, len(s2), cap(s2))  // [0 0 0 0 0 0] 6 8

   s3 := make([]int, 6) // 省略 cap,相当于 cap = len。
   fmt.Println(s3, len(s3), cap(s3))  // [0 0 0 0 0 0] 6 6
}

使用 make 动态创建slice,避免了数组必须用常量做长度的麻烦。还可以用指针直接访问底层数组,退化成普通数组操作:

package main

import "fmt"

func main() {
   s := []int{0,1,2,3,4}
   p := &s[2]
   *p = *p+100
   fmt.Println(s) // [0 1 102 3 4]
}

[][]T,是指元素类型为 []T

package main

import "fmt"

func main() {
   data:=[][]int{
      []int{1,2,3},
      []int{4,5},
      []int{6,7,8,9},
   }
   fmt.Println(data)
}

可直接修改 structarrayslice 成员:

package main

import "fmt"

func main() {
   student := [3]struct{
      age int
      name string
   }{}

   s:=student[:]
   student[1].age = 20
   s[2].age = 18
   fmt.Println(student)   // [{0 } {20 } {18 }]
   fmt.Println(s)       // [{0 } {20 } {18 }]
}

append切片追加

用append内置函数操作切片(切片追加).

append :向 slice 尾部添加数据,返回新的 slice 对象

看个例子就能懂:

package main

import "fmt"

func main() {
   var a = []int{1,2,3}
   fmt.Printf("%v\n", a)  // [1 2 3]
   b := []int{4,5,6}
   fmt.Printf("%v\n", b)  // [4 5 6]
   c := append(a, b...)   // ...表示照单全收
   fmt.Printf("%v\n", c)  // [1 2 3 4 5 6]
   d := append(a, 7)
   fmt.Printf("%v\n", d)  // [1 2 3 7]
   e := append(b,5,6,7)
   fmt.Printf("%v\n", e)  // [4 5 6 5 6 7]
}

slice扩容

切片超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满。

package main

import "fmt"

func main() {
   arr := [...]int{0,1,2,3,4, 10: 100}       // 定义数组
   fmt.Println(arr)   // [0 1 2 3 4 0 0 0 0 0 100]
   s := arr[:2:3] // low:high:max
   fmt.Println(s) // [0 1]
   fmt.Println(&arr[0],&s[0]) // 0xc000086060 0xc000086060(此时底层数组是同一个)
   s = append(s, 101,102) // 一下append2个,cap超过3了,重新分配底层数组,与原数组无关。
   fmt.Println(s, arr)       // [0 1 101 102] [0 1 2 3 4 0 0 0 0 0 100]
   fmt.Println(&arr[0],&s[0]) // 0xc000068060 0xc00000c300(此时s的底层数组以及是新的数组了)
}

从输出结果可以看出,append 后的 s 重新分配了底层数组,并复制数据。 如果只追加一个值,则不会超过 s.cap 限制,也就不会重新分配。 通常以 2 倍容量重新分配底层数组在大批量添加数据时,建议一次性分配足够大的空间,以减少内存分配和数据复制开销。 或初始化足够长的 len 属性,改用索引号进行操作。及时释放不再使用的 slice 对象,避免持有过期数组,造成 GC 无法回收。

可以通过代码看一下slice中cap重新分配规律:

package main

import "fmt"

func main() {
   s := make([]int, 0, 1)
   c := cap(s)

   for i := 0; i < 50; i++ {
      s = append(s, i)
      if n := cap(s); n > c {
         fmt.Printf("cap: %d -> %d\n", c, n)
         c = n
      }
   }
}

结果如下:

slice复制

copy :函数 copy 在两个 slice 间复制数据,复制长度以 len 小的为准。两个 slice 可指向同一底层数组,允许元素区间重叠。

看一下go源码的注释说明:

// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int

看个例子:

package main

import "fmt"

func main() {
   s1 := []int{1,2,3,4,5,6}
   fmt.Println(s1)       // [1 2 3 4 5 6]
   s2 := make([]int, 4)
   fmt.Println(s2)       // [0 0 0 0]
   copy(s1, s2)
   fmt.Println(s1,s2)    // [0 0 0 0 5 6] [0 0 0 0]
}

再看一个两个 slice 指向同一底层数组的例子:

package main

import "fmt"

func main() {
   data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   fmt.Println("底层数组:", data) // 底层数组: [0 1 2 3 4 5 6 7 8 9]
   s1 := data[8:]
   fmt.Println(s1) // [8 9]
   s2 := data[:5]
   fmt.Println(s2) // [0 1 2 3 4]
   copy(s2, s1)
   fmt.Println(s1)	// [8 9]
   fmt.Println(s2)	// [8 9 2 3 4]
   fmt.Println("底层数组:", data) // 底层数组: [8 9 2 3 4 5 6 7 8 9]
}

应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

切片遍历

看个例子就行:

package main

import "fmt"

func main() {
   s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   for index, value := range s {
      fmt.Println(index, value)
   }
}

结果如下:

数组和切片的内存布局

先看个例子:

package main

import "fmt"

func main() {
   var a = []int{0, 1, 2, 3, 4}
   fmt.Println(a, len(a)) // [0 1 2 3 4] 5
   b := a[1:2]
   fmt.Println(b, len(b)) // [1] 1
   c := b[0:3]
   fmt.Println(c, len(c)) // [1 2 3] 3
}

为什么切片b的长度只有1,却能通过c := b[0:3]来得到一个长度为3的切片c呢,这就要看一下数组和切片的内存布局:

字符串和切片(string and slice)

string底层就是一个byte的数组,因此,也可以进行切片操作。

package main

import "fmt"

func main() {
   str:="hello,world"
   fmt.Printf("%c\n", str[4]) // o
   s1:=str[0:5]
   fmt.Println(s1)    // hello
   s2:=str[6:]
   fmt.Println(s2)    // world
}

string本身是不可变的,因此要改变string中字符(如实现str[4]='m'的操作)。需要如下操作:

package main

import "fmt"

func main() {
   str := "hello,world"
   fmt.Println(str) // hello,world
   s := []byte(str) // 中文字符需要用[]rune(str)
   s[4] = 'm'
   str = string(s)
   fmt.Println(str) // hellm,world

}

对于含有中文字符串,需要用[]rune(str)

package main

import "fmt"

func main() {
   str := "你好,世界!hello world!"
   s := []rune(str)
   s[3] = '够'
   s[4] = '浪'
   s[12] = 'g'
   s = s[:14]
   str = string(s)
   fmt.Println(str) //你好,够浪!hello go
}

golang slice data[:6:8] 两个冒号的理解:

  • 常规slice , data[6:8],从第6位到第8位(返回6, 7),长度len为2, 最大可扩充长度cap为4(6-9)
  • 另一种写法: data[:6:8] 每个数字前都有个冒号, slice内容为data从0到第6位,长度len为6,最大扩充项cap设置为8

a[x:y:z]:切片内容 [x:y],切片长度: y-x,切片容量:z-x。

package main

import "fmt"

func main() {
   slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   d1 := slice[6:8]
   fmt.Println(d1, len(d1), cap(d1)) // [6 7] 2 4
   d2 := slice[:6:8]
   fmt.Println(d2, len(d2), cap(d2)) // [0 1 2 3 4 5] 6 8
}
package main

import (
   "fmt"
   "strings"
)

func main() {
   slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   fmt.Println(slice) // [0 1 2 3 4 5 6 7 8 9]
   str := strings.Replace(strings.Trim(fmt.Sprint(slice), "[]"), " ", ",", -1)
   fmt.Println(str)   // 0,1,2,3,4,5,6,7,8,9
}

数组or切片转字符串

     strings.Replace(strings.Trim(fmt.Sprint(array_or_slice), "[]"), " ", ",", -1)
  • fmt.Sprint():Go语言格式的fmt.Sprint()函数使用其操作数的默认格式,并返回结果字符串。当任何字符串不用作常量时,此处在操作数之间添加空格(这里的作用时将slice类型转为string类型,方便后续Trim操作)

  • (strings.Trim(s, cutset string):返回将 s 前后端所有 cutset 包含的 utf-8 码值都去掉的字符串(这里就去掉了前端的[和后端的]

  • strings.Replace(s, old, new string, n int):这里的作用时将原字符的所有空格" "替换成逗号",",-1表示替换所有的空格。

    看一下相关源码:

     // Sprint formats using the default formats for its operands and returns the resulting string.
     // Spaces are added between operands when neither is a string.
     func Sprint(a ...interface{}) string {
         ...
     }
     ​
     // Trim returns a slice of the string s with all leading and
     // trailing Unicode code points contained in cutset removed.
     func Trim(s, cutset string) string {
         if s == "" || cutset == "" {
             return s
         }
         return TrimFunc(s, makeCutsetFunc(cutset))
     }
     ​
     // Replace returns a copy of the string s with the first n
     // non-overlapping instances of old replaced by new.
     // If old is empty, it matches at the beginning of the string
     // and after each UTF-8 sequence, yielding up to k+1 replacements
     // for a k-rune string.
     // If n < 0, there is no limit on the number of replacements.
     func Replace(s, old, new string, n int) string {
         ...   
     }
    

例子:

package main

import (
   "fmt"
   "strings"
)

func main() {
   slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
   fmt.Println(slice) // [0 1 2 3 4 5 6 7 8 9]
   str := strings.Replace(strings.Trim(fmt.Sprint(slice), "[]"), " ", ",", -1)
   fmt.Println(str)   // 0,1,2,3,4,5,6,7,8,9
}

Slice底层实现

Slice底层实现

(面试常问)


Map

Map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

map定义

Go语言中 map的定义语法如下:

    map[KeyType]ValueType
  • KeyType:表示键的类型
  • ValueType:表示键对应的值的类型

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

    make(map[KeyType]ValueType, cap)

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。

map基本使用

map中的数据都是成对出现的,map的基本使用示例代码如下:

package main

import "fmt"

func main() {
   ageMap := make(map[string]int, 10)
   ageMap["张三"] = 18
   ageMap["李四"] = 20
   ageMap["Rick"] = 58
   fmt.Println(ageMap)       // map[Rick:58 张三:18 李四:20](key时无序的)
   fmt.Println(ageMap["李四"]) // 20
}

map也支持在声明的时候填充元素,例如:

package main

import "fmt"

func main() {
   login := map[string]string{
      "username": "lisi",
      "password": "123456",
   }
   fmt.Println(login) // map[password:123456 username:lisi]
}

判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,格式如下:

    value, ok := map[key]

看个例子:

package main

import "fmt"

func main() {
   ageMap := make(map[string]int, 10)
   ageMap["张三"] = 18
   ageMap["李四"] = 20
   ageMap["Rick"] = 58
   value, ok := ageMap["Rick"] // 如果key存在,ok为true,v为对应的值;如果key不存在,ok为false,v为值类型的零值
   if ok {
      fmt.Println(value)
   } else {
      fmt.Println("查无此人")
   }
}

map的遍历

Go语言中使用for range遍历map。如下:

package main

import "fmt"

func main() {
   ageMap := make(map[string]int, 10)
   ageMap["张三"] = 18
   ageMap["李四"] = 20
   ageMap["Rick"] = 58
   for k, v := range ageMap {
      fmt.Println(k, v)
   }
}

只遍历key或value:

package main

import "fmt"

func main() {
   ageMap := make(map[string]int, 10)
   ageMap["张三"] = 18
   ageMap["李四"] = 20
   ageMap["Rick"] = 58
   // 只遍历key
   for k := range ageMap {
      fmt.Println(k)
   }
   // 只遍历value
   for _,v := range ageMap {
      fmt.Println(v)
   }
}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。(map是无序的)

使用delete()函数删除键值对

使用delete()函数从map中删除一组键值对,delete()函数的格式如下:

    delete(map, key)

其中:

  • map:表示要删除键值对的map
  • key: 表示要删除的键值对的键

示例如下:

package main

import "fmt"

func main() {
   ageMap := make(map[string]int, 10)
   ageMap["张三"] = 18
   ageMap["李四"] = 20
   ageMap["Rick"] = 58
   for k, v := range ageMap {
      fmt.Println(k,v)   // 删除前
   }
   delete(ageMap, "Rick")
   for k, v := range ageMap {
      fmt.Println(k,v)   // 删除后
   }
}

按照指定顺序遍历map

示例如下:

package main

import (
	"fmt"
	"math/rand"
	"sort"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano()) // 初始化随机数种子
	scoreMap := make(map[string]int, 100)
	for i := 0; i < 10; i++ {
		key := fmt.Sprintf("stu%02d", i) // 生成stu开头的字符串
		value := rand.Intn(100)          // 生成0~99的随机整数
		scoreMap[key] = value
	}
	// 取出map中的所有key存入切片keys
	keys := make([]string, 0, 100)
	for key := range scoreMap {
		keys = append(keys, key)
	}
	sort.Strings(keys) // 对切片进行排序
	// 按照排序后的key遍历map
	for _, key := range keys {
		fmt.Println(key, scoreMap[key])
	}
	fmt.Println("-----------------------------------------------")
	// 无序的遍历map
	for k, v := range scoreMap {
		fmt.Println(k, v)
	}
}

元素为map类型的切片

示例如下:

package main

import "fmt"

func main() {
   mapSlice := make([]map[string]string, 3)
   for i, v := range mapSlice {
      fmt.Println(i, v) // map初始化之前
   }
   fmt.Println("-----------------------------------")

   mapSlice[0] = make(map[string]string, 10)
   mapSlice[0]["name"] = "张三"
   mapSlice[0]["male"] = "女"
   mapSlice[0]["password"] = "111"
   for i, v := range mapSlice {
      fmt.Println(i, v) // map初始化之后
   }
}

值为切片类型的map

示例如下:

package main

import "fmt"

func main() {
   sliceMap := make(map[string][]string, 3)
   fmt.Println(sliceMap)
   value := make([]string, 3)
   value = append(value, "深圳", "上海")
   sliceMap["Chian"] = value
   fmt.Println(sliceMap)  // after init
}

Map实现原理

Map实现原理

(面试常问)


Go指针

指针

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

要搞明白Go语言中的指针需要先知道3个概念:

  • 指针地址
  • 指针类型
  • 指针取值

Go语言中的指针

Go语言中的函数传参都是值传递,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号

  • &(取地址)
  • *(根据地址取值)

Go语言参数传递是值传递还是引用传递

最终我们可以确认的是Go语言中所有的传参都是值传递,都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。

是否可以修改原内容数据,和值传递、引用传递没有必然的关系。在C++中,传引用肯定是可以修改原内容数据的,在Go语言里,虽然只有传值,但是我们也可以修改原内容数据,因为参数是引用类型。

请记住:

  • Go里只有值传递
  • 引用传递和引用类型是两个概念

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行取地址操作。 Go语言中的值类型(intfloatboolstringarraystruct)都有对应的指针类型,如:*int*int64*string等。

取变量指针的语法如下:

     ptr := &v    // v的类型为T

其中:

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

举个例子:

 package main
 ​
 import "fmt"
 ​
 func main() {
     a := 10
     b := &a
     fmt.Println(a, &a)                   // 10 0xc0000aa058
     fmt.Printf("b: %p type: %T\n", b, b) // b: 0xc00000a0a8 type: *int
     fmt.Println(&b)                      // 0xc000006028
 }

我们来看一下b := &a的图示:

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值。

示例如下:

package main

import "fmt"

func main() {
   a := 10
   b := &a
   c := *b
   fmt.Printf("type of b:%T\n", b)           // type of b:*int
   fmt.Printf("type of c:%T, c :%v\n", c, c) // type of c:int, c :10
}

总结: 取地址操作符&和取值操作符*是一对互补操作符:

  • &取出地址
  • *根据地址取出地址指向的值

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址&操作,可以获得这个变量的指针变量
  • 指针变量的值是指针地址
  • 对指针变量进行取值*操作,可以获得指针变量指向的原变量的值

指针传值示例:

package main

import "fmt"

func main() {
   a := 10
   modify1(a)
   fmt.Println(a) // 10
   modify2(&a)
   fmt.Println(a) // 100
}

func modify1(x int) {
   x = 100
}
func modify2(x *int) { // 指针传值
   *x = 100
}

空指针

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

空指针的判断:p==nil进行判断即可。

new和make

先看一个例子:

package main

import "fmt"

func main() {
   var a *int
   *a = 100
   fmt.Println(a)

   var b map[string]int
   b["math"] = 90
   fmt.Println(b)
}

执行上面的代码会引发panic,因为在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间

要分配内存,就引出来newmake。 Go语言中newmake是内建的两个函数,主要用来分配内存

new

new是一个内置的函数,在go源码中它的函数签名如下:

 // The new built-in function allocates memory. The first argument is a type,
 // not a value, and the value returned is a pointer to a newly
 // allocated zero value of that type.
 func new(Type) *Type

其中:

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型
  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。

举个例子:

 package main
 ​
 import "fmt"
 ​
 func main() {
    i := new(int)
    fmt.Println(*i) // 0
    *i = 10
    fmt.Println(*i) // 10
    b := new(bool)
    fmt.Println(*b) // false
 }

在最上面的示例代码var a *int中只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值

按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值了:

package main

import "fmt"

func main() {
   var a *int
   a = new(int)
   *a = 11
   fmt.Println(*a) // 11
}

make

make也是用于内存分配的,区别于new,它只用于slicemap以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。

make函数的函数签名如下:

func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

示例如下:

package main

import "fmt"

func main() {
   var b map[string]int
   b = make(map[string]int, 10)
   b["math"] = 90
   fmt.Println(b) // map[math:90]
   m := make(map[string]int)
   m["english"] = 100
   fmt.Println(m) // map[english:100]
}

new与make的区别

  • newmake都是用来做内存分配的
  • make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
  • new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

结构体

结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

Go语言中通过struct来实现面向对象。

结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

    type 类型名 struct {
        字段名 字段类型
        字段名 字段类型
        ...
    }

其中:

  1. 类型名:标识自定义结构体的名称,在同一个包内不能重复;
  2. 字段名:表示结构体字段名。结构体中的字段名必须唯一;
  3. 字段类型:表示结构体字段的具体类型。

看个例子就行:

type person struct {
   name string
   age uint8
   city string
}

同样类型的字段可以写在一行:

type person struct {
   name, city string
   age uint8
}

结构体实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。

结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型:

    var 结构体实例 结构体类型

基本实例化

示例如下:

package main

import "fmt"

type person struct {
	name string
	age  uint8
	city string
}

func main() {
	p1 := person{
		name: "张三",
		age:  18,
		city: "深圳",
	}
	var p2 person
	p2.name = "李四"
	p2.age = 20
	p2.city = "杭州"
	fmt.Println(p1)      // {张三 18 深圳}
	fmt.Println(p2)      // {李四 20 杭州}
	fmt.Println(p1.name) // 张三
}

我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。

创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。

如下:

package main

import "fmt"

type person struct {
	name string
	age  uint8
	city string
}

func main() {
	p := new(person)
	fmt.Printf("%T\n", p)    // *main.person
	fmt.Printf("p=%#v\n", p) // p=&main.person{name:"", age:0x0, city:""}
	p.name = "张三"
	p.age = 18
	p.city = "深圳"
	fmt.Println(*p) // {张三 18 深圳}
}

从打印的结果中我们可以看出2是一个结构体指针(*main.person)。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。(这是Go语言帮我们实现的语法糖)

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

示例如下:

package main

import "fmt"

type person struct {
	name string
	age  uint8
	city string
}

func main() {
	p := &person{}
	fmt.Printf("%T\n", p)    // *main.person
	fmt.Printf("p=%#v\n", p) // p=&main.person{name:"", age:0x0, city:""}
	p.name = "jack"
	fmt.Println(p) // &{jack 0 }
}

p.name = "jack"其实在底层是(*p).name = "jack",这是Go语言帮我们实现的语法糖。

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

示例如下:

package main

import "fmt"

func main() {
   var user struct {
      name string
      age  int
   }
   user.name = "张三"
   user.age = 18
   fmt.Println(user) // {张三 18}
}

结构体初始化

声明一个结构体不进行初始化,如下:

package main

import "fmt"

type person struct {
   name string
   age  uint8
   city string
}

func main() {
   var p person
   fmt.Printf("p=%#v\n", p) // p=main.person{name:"", age:0x0, city:""}
}

使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。如下:

package main

import "fmt"

type person struct {
   name string
   age  uint8
   city string
}

func main() {
   p := person{
      name: "张三",
      age:  20,
      city: "深圳",
   }
   fmt.Println(p) // {张三 20 深圳}
}

也可以对结构体指针进行键值对初始化,例如:

package main

import "fmt"

type person struct {
   name string
   age  uint8
   city string
}

func main() {
   p := &person{
      name: "张三",
      age:  20,
      city: "深圳",
   }
   fmt.Println(*p) // {张三 20 深圳}
}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值

使用值的列表初始化

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值:

package main

import "fmt"

type person struct {
   name string
   age  uint8
   city string
}

func main() {
   p := person{
      "tom",
      21,
      "深圳",
   }
   fmt.Println(p) // {tom 21 深圳}
}

使用这种格式初始化时,需要注意:

  • 必须初始化结构体的所有字段
  • 初始值的填充顺序必须与字段在结构体中的声明顺序一致
  • 该方式不能和键值初始化方式混用

结构体内存布局

看个例子:

 package main
 ​
 import "fmt"
 ​
 type person struct {
    name string
    age  uint8
    city string
 }
 ​
 func main() {
    p := person{
       "tom",
       21,
       "深圳",
    }
    fmt.Printf("%p\n", &p)      // 0xc000090480
    fmt.Printf("%p\n", &p.name) // 0xc000090480
    fmt.Printf("%p\n", &p.age)  // 0xc000090490
    fmt.Printf("%p\n", &p.city) // 0xc000090498
 }

再看一个示例:

 package main
 ​
 import "fmt"
 ​
 type test struct {
    a int8
    b int8
    c int8
    d int8
 }
 ​
 func main() {
    n := test{
       1, 2, 3, 4,
    }
    fmt.Printf("n.a %p\n", &n.a) // n.a 0xc00000a0a8
    fmt.Printf("n.b %p\n", &n.b) // n.b 0xc00000a0a9
    fmt.Printf("n.c %p\n", &n.c) // n.c 0xc00000a0aa
    fmt.Printf("n.d %p\n", &n.d) // n.d 0xc00000a0ab
 }

Map+Struct的Demo

代码如下:

 package main
 ​
 import "fmt"
 ​
 type student struct {
    name string
    age  int
 }
 ​
 func main() {
    m := make(map[string]student)
    stus := []student{
       {name: "张三", age: 18},
       {name: "losing", age: 23},
       {name: "jack", age: 28},
    }
    for _, stu := range stus {
       m[stu.name] = stu
    }
    for k, v := range m {
       fmt.Println(k, "-->", v)
    }
 }

运行结果如下:

 张三 --> {张三 18}
 losing --> {losing 23}
 jack --> {jack 28}

结构体的构造函数(自己实现的)

Go语言的结构体没有构造函数,我们可以自己实现

例如:

 package main
 ​
 import "fmt"
 ​
 type student struct {
     name string
     age  int
 }
 ​
 // 自定义的构造函数
 func newStudent(name string, age int) *student {
     return &student{
         name: name,
         age:  age,
     }
 }
 ​
 func main() {
     s := newStudent("张三", 18) // 调用构造函数
     fmt.Println(*s)
 }

上面的代码就实现了一个student的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型

方法和接收者

关于Go的函数和方法后续会更详细的提到,这里简单讲一下。

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者self

方法的定义格式如下:

  func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
         函数体
     }

其中:

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

 package main
 ​
 import "fmt"
 ​
 type student struct {
    name string
    age  int
 }
 ​
 // 自定义的构造函数
 func newStudent(name string, age int) *student {
    return &student{
       name: name,
       age:  age,
    }
 }
 ​
 // 专属于Student类的方法
 func (s student) doWhat(work string) {
    fmt.Println("今天的工作是:"+work)
 }
 ​
 func main() {
    s := newStudent("张三", 18) // 调用构造函数
    fmt.Println(s) // &{张三 18}
    s.doWhat("学习") // 今天的工作是:学习
 }

方法与函数的区别是:函数不属于任何类型,方法属于特定的类型。

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。

流程控制

条件语句if

条件语句if

没什么好说的,直接看个demo:

package main

import "fmt"

func main(){
   var num int
   fmt.Printf("请输入一个1-6的数字:")
   fmt.Scan(&num)
   if num>=1&&num<=3 {
      fmt.Println("小")
   }else if num>=4&&num<=6 {
      fmt.Println("大")
   }else {
      fmt.Println("输入有误")
   }
}

注意:代码块左括号{必须在条件表达式尾部! 这是因为go会自动补充;的原因。

条件语句switch

条件语句switch

switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。 Golang switch 分支表达式可以是任意类型,不限于常量。可省略 break,默认自动终止。

Go 编程语言中 switch 语句的语法如下:

switch var1 {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。 可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3

给个例子:

package main

import "fmt"

func main(){
   var num int
   fmt.Printf("请输入一个1-6的数字:")
   fmt.Scan(&num)
   switch num {
   case 1:
      fmt.Println("小")
   case 2, 3:
      fmt.Println("小")
   case 4, 5, 6:
      fmt.Println("大")
   default:
      fmt.Println("输入有误!")
   }
}

补充:fallthrough关键字的使用:Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码(这里只执行后一个case)。

go语言fallthrough的用法心得

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

Type Switch 语法格式如下:

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

举个例子:

package main

import "fmt"

func main(){
   var x interface{}
   //写法一:
   switch i := x.(type) { // 带初始化语句
   case nil:
      fmt.Printf(" x 的类型 :%T\r\n", i)
   case int:
      fmt.Printf("x 是 int 型")
   case float64:
      fmt.Printf("x 是 float64 型")
   case func(int) float64:
      fmt.Printf("x 是 func(int) 型")
   case bool, string:
      fmt.Printf("x 是 bool 或 string 型")
   default:
      fmt.Printf("未知型")
   }
}

运行结果为: x 的类型 :<nil>

Go的switch非常灵活,表达式可以是常量也可以是整数,执行的过程从上至下,直到找到匹配项;而如果switch没有表达式,它会匹配true。

Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch,但是可以使用fallthrough强制执行后面的case代码。

条件语句select

条件语句select

select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

Go 编程语言中 select 语句的语法如下:

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

每个case都必须是一个通信,所有channel表达式都会被求值,所有被发送的表达式都会被求值。

如果任意某个通信可以进行,它就执行;其他被忽略。如果有多个case都可以运行,Select会随机公平地选出一个执行,其他不会执行。

否则:如果有default子句,则执行该语句;如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

(具体参考条件语句select,后面需要用到了在进行补充)

循环语句for

循环语句for

Golang的循环只有for。 for支持三种循环方式,包括类似 while 的语法。

语法:

  • for init; condition; post { }(类似Java的普通for循环)
  • for condition { }(类似Java的while(condition)循环)
  • for { }(类似Java的while(true)循环)

语法没什么好说的,看一个输出100以内全部素数的结果的例子即可:

package main

import "fmt"

func main(){
   /* 定义局部变量 */
   var i, j int
   for i = 2; i < 100; i++ {
      for j = 2; j < i; j++ {    // 可优化
         if i%j==0 {
            break
         }
      }
      if j==i {  // 说明走完嵌套内的循环而没有中途break
         fmt.Println(i,"是素数")
      }
   }
}

循环语句range

循环语句range

range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。

for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

1st value2nd value
stringindexs[index]unicode, rune
array/sliceindexs[index]
mapkeym[key]
channelelement

可忽略不想要的返回值,使用 _ 即可。

例如:

package main

import "fmt"

func main(){
   s := "hello"
   // 忽略 2nd value,支持 string/array/slice/map。
   for i := range s {
      fmt.Println(s[i])
   }
   // 忽略 index。
   for _, c := range s {
      fmt.Println(c)
   }
   // 忽略全部返回值,仅迭代。
   for range s {

   }

   m := map[string]int{"hello":1, "world":2}
   for k,v := range m {
      fmt.Println(k,v)
   }
}

注意:range 会复制对象。建议改用引用类型,其底层数据不会被复制。具体例子可以参考循环语句range

forfor range的区别主要体现在使用场景不同:

  • for可以:

    • 遍历array和slice
    • 遍历key为整型递增的map
    • 遍历string
  • for range可以完成所有for可以做的事情,也能做到for不能做的,包括:

    • 遍历key为string类型的map并同时获取key和value
    • 遍历channel

循环控制goto、break、continue

循环控制语句可以控制循环体内语句的执行过程。GO 语言支持以下几种循环控制语句:gotobreakcontinue

一般推荐不使用gotobreakcontinue没什么好说的。