Go学习语法Golang

go 专栏收录该内容
9 篇文章 0 订阅

1.安装

https://studygolang.com/dl

2.使用vscode编辑器安装go插件

3.go语法

_是go的空白标识符,忽视用的 结尾不需要”;”编译自动加

package main //包名  mian包表示可独立执行的程序 包名可以不和目录名一致  每个目录一个包
import "fmt" //导入标准库包 这个是目录路径  全局 ./相对目录  /根目录查找
/* 第二种导入方法 多行注释
import fm "fmt" // 别名导入
import (  
    "fmt"
    "os"
 )
 */
 //init特殊的函数,每个含有该函数的包都会首先执行这个函数
func init(){

}
//主执行函数
func main() {
    fmt.Println("hello, world")
}

3.1类型

类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、slice、map、channel;只描述类型的行为的,如:interface。

结构化的类型没有真正的值,它使用 nil 作为默认值。Go 语言中不存在类型继承
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来,如:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
return var1, var2

3.2常量

常量的定义格式:const identifier [type] = value
const Pi = 3.14159
在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。
常量还可以用作枚举:

const (
    Unknown = 0
    Female = 1
    Male = 2
)

在这个例子中,iota 可以被用作枚举值:

const (
    a = iota
    b = iota
    c = iota
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

const (
    a = iota
    b
    c
)

在每遇到一个新的常量块或单个常量声明时, iota 都会重置为 0

3.3变量

声明变量的一般形式是使用 var 关键字:var identifier type。

//这种因式分解关键字的写法一般用于声明全局变量。
var (
    a int
    b bool
    str string
)

当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。
变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate。
但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写
当你在函数体内声明局部变量时,应使用简短声明语法 :=,例如:a := 1

所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:像数组和结构这些复合类型也是值类型
当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝:你可以通过 &i 来获取变量 i 的内存地址

在 Go 语言中,指针属于引用类型,其它的引用类型还包括 slices,maps和 channel。被引用的变量会存储在堆中,以便进行垃圾回收,且比栈拥有更大的内存空间。
简短形式,使用 := 赋值操作符
这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。

3.4基本类型和运算符

var b bool = true
Go 拥有以下复数类型:
complex64 (32 位实数和虚数)
complex128 (64 位实数和虚数)
复数使用 re+imI 来表示,其中 re 代表实数部分,im 代表虚数部分,I 代表根号负 1。

var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 输出: 5 + 10i

一些像游戏或者统计学类的应用需要用到随机数。rand 包实现了伪随机数的生成。
类型别名
在 type TZ int 中,TZ 就是 int 类型的新名称(用于表示程序中的时区),然后就可以使用 TZ 来操作 int 类型的数据。
实际上,类型别名得到的新类型并非和原类型完全相同,新类型不会拥有原类型所附带的方法
字符类型
var ch byte = 65 或 var ch byte = '\x41'

3.5字符串

字符串的内容(纯字节)可以通过标准索引法来获取,在中括号 [] 内写入索引,索引从 0 开始计数:
- 字符串 str 的第 1 个字节:str[0]
- 第 i 个字节:str[i - 1]
- 最后 1 个字节:str[len(str)-1]
需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意事项 获取字符串中某个字节的地址的行为是非法的,例如:&str[i]。
在循环中使用加号 + 拼接字符串并不是最高效的做法,更好的办法是使用函数 strings.Join()
strings 和 strconv 包
HasPrefix 判断字符串 s 是否以 prefix 开头:
strings.HasPrefix(s, prefix string) bool
Contains 判断字符串 s 是否包含 substr:
strings.Contains(s, substr string) bool
Index 返回字符串 str 在字符串 s 中的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str:
strings.Index(s, str string) int
Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new:
strings.Replace(str, old, new, n) string
strings.ToLower(s) string //换为相应的小写字符
strings.TrimSpace(s)// 来剔除字符串开头和结尾的空白符号
strings.Split(s, sep)用于自定义分割符号来对指定字符串进行分割,同样返回 slice。
Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串:
strings.Join(sl []string, sep string) string

3.6时间和日期

time.Now()

3.7指针

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。

var i1 = 5
fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1)
var intP *int
intP = &i1

一个指针变量可以指向任何一个值的内存地址 它指向那个值的内存地址在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节
对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)。

4.控制结构

  • if-else 结构
  • switch 结构
  • select 结构,用于 channel 的选择

4.1if-else 结构

if condition1 {
    // do something    
} else if condition2 {
    // do something else    
}else {
    // catch-all or default
}
if initialization; condition {
    // do something
}
if value := process(data); value > max {
    ...
}

4.2 多返回值

value, err := pack1.Function1(param1)
anInt, _ = strconv.Atoi(origStr)

4.3 switch结构

switch var1 {
    case val1:
        ...
    case val2,val3,val4:
    case 0: // 空分支,只有当 i == 0 时才会进入分支
    case 0: fallthrough //执行下一个分支的代码
        ...
    default:
        ...
}

类似 if-else

switch {
    case i < 0:
        f1()
    case i == 0:
        f2()
    case i > 0:
        f3()
}

任何支持进行相等判断的类型都可以作为测试表达式的条件,包括 int、string、指针等。

//变量 a 和 b 被平行初始化,然后作为判断条件:
switch a, b := x[i], y[j]; {
    case a < b: t = -1
    case a == b: t = 0
    case a > b: t = 1
}

4.4 for 结构

4.4.1基于计数器的迭代

for 初始化语句; 条件语句; 修饰语句 {}

//示例
 for i := 0; i < 5; i++ {
        fmt.Printf("This is the %d iteration\n", i)
    }

特别注意,永远不要在循环体内修改计数器,这在任何语言中都是非常差的实践!
您还可以在循环中同时使用多个计数器:
for i, j := 0, N; i < j; i, j = i+1, j-1 {}

4.4.2基于条件判断的迭代

for 结构的第二种形式是没有头部的条件判断迭代(类似其它语言中的 while 循环),基本形式为:for 条件语句 {}。

4.4.3无限循环

条件语句是可以被省略的,如 i:=0; ; i++ 或 for { } 或 for ;; { }(;; 会在使用 gofmt 时被移除):这些循环的本质就是无限循环。最后一个形式也可以被改写为 for true { },但一般情况下都会直接写 for { }。
想要直接退出循环体,可以使用 break 语句或 return 语句直接返回
break 只是退出当前的循环体,而 return 语句提前对函数进行返回
无限循环的经典应用是服务器,用于不断等待和接受新的请求。

for t, err = p.Token(); err == nil; t, err = p.Token() {
    ...
}
4.4.4for-range 结构

这是 Go 特有的一种的迭代结构
语法上很类似其它语言中 foreach 语句
一般形式为:for ix, val := range coll { }。
一个字符串是 Unicode 编码的字符(或称之为 rune)集合,因此您也可以用它迭代字符串:

for pos, char := range str {
...
}

4.5Break 与 continue

break 语句退出当前循环。
关键字 continue 忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。
另外,关键字 continue 只能被用于 for 循环中。

4.6标签与 goto

for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词
(标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母)

LABEL1:
    for i := 0; i <= 5; i++ {
        for j := 0; j <= 5; j++ {
            if j == 4 {
                continue LABEL1
            }
            fmt.Printf("i is: %d, and j is: %d\n", i, j)
        }
    }

特别注意 使用标签和 goto 语句是不被鼓励的:它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方案来实现相同的需求。
如果您必须使用 goto,应当只使用正序的标签(标签位于 goto 语句之后),但注意标签和 goto 语句之间不能出现定义新变量的语句,否则会导致编译失败。

5函数

Go 里面有三种类型的函数:

  • 普通的带有名字的函数
  • 匿名函数或者lambda函数
  • 方法(Methods)

假设 f1 需要 3 个参数 f1(a, b, c int),同时 f2 返回 3 个参数 f2(a, b int) (int, int, int),就可以这样调用 f1:f1(f2(a, b))。

函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参与/或者不同的返回值,在 Go 里面函数重载是不被允许的。这将导致一个编译错误:
Go 语言不支持这项特性的主要原因是函数重载需要进行多余的类型匹配影响性能;没有重载意味着只是一个简单的函数调度。所以你需要给不同的函数使用不同的名字,我们通常会根据函数的特征对函数进行命名
如果需要申明一个在外部定义的函数,你只需要给出函数名与函数签名,不需要给出函数体:

func flushICache(begin, end uintptr) // implemented externally
函数也可以以申明的方式被使用,作为一个函数类型,就像:

type binOp func(int, int) int
在这里,不需要函数体 {}。

5.1函数参数与返回值

函数定义时,它的形参一般是有名字的,不过我们也可以定义没有形参名的函数,只有相应的形参类型,就像这样:func f(int, int, float64)。
没有参数的函数通常被称为 niladic 函数(niladic function),就像 main.main()。

按值传递(call by value) 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
如果一个函数需要返回四到五个值,我们可以传递一个切片给函数(如果返回值具有相同类型)或者是传递一个结构体(如果返回值具有不同的类型)。因为传递一个指针允许直接修改变量的值,消耗也更少。
命名的返回值(named return variables)
命名返回值作为结果形参(result parameters)被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来

func getX2AndX3(input int) (int, int) {
    return 2 * input, 3 * input
}

func getX2AndX3_2(input int) (x2 int, x3 int) {
    x2 = 2 * input
    x3 = 3 * input
    // return x2, x3
    return
}

尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂
空白符(blank identifier)
空白符用来匹配一些不需要的值,然后丢弃掉,
i1, _, f1 = ThreeValues()

5.2传递变长参数

如果函数的最后一个参数是采用 …type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。
func myFunc(a, b, arg ...int) {}
如果参数被存储在一个数组 arr 中,则可以通过 arr… 的形式来传递参数调用变参函数。

package main

import "fmt"

func main() {
    x := min(1, 3, 2, 0)
    fmt.Printf("The minimum is: %d\n", x)
    arr := []int{7,9,3,5,1}
    x = min(arr...)
    fmt.Printf("The minimum in the array arr is: %d", x)
}

func min(a ...int) int {
    if len(a)==0 {
        return 0
    }
    min := a[0]
    for _, v := range a {
        if v < min {
            min = v
        }
    }
    return min
}

但是如果变长参数的类型并不是都相同的呢
1.使用结构

type Options struct {
     par1 type1,
     par2 type2,
     ...
 }

2.使用空接口:
使用默认的空接口 interface{},这样就可以接受任何类型的参数

5.3defer 和追踪

关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。
当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)

func f() {
    for i := 0; i < 5; i++ {
        defer fmt.Printf("%d ", i) //4 3 2 1 0
    }
}

关键字 defer 允许我们进行一些函数执行完成后的收尾工作
1.关闭文件流 defer file.Close()
2.解锁一个加锁的资源
mu.Lock()
defer mu.Unlock()
3.打印最终报告defer printFooter()
4.关闭数据库链接 defer disconnectFromDB()
使用 defer 语句实现代码追踪

package main

import "fmt"

func trace(s string) string {
    fmt.Println("entering:", s)
    return s
}

func un(s string) {
    fmt.Println("leaving:", s)
}

func a() {
    defer un(trace("a"))
    fmt.Println("in a")
}

func b() {
    defer un(trace("b"))
    fmt.Println("in b")
    a()
}

func main() {
    b()
}

使用 defer 语句来记录函数的参数与返回值

package main

import (
    "io"
    "log"
)

func func1(s string) (n int, err error) {
    defer func() {
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, io.EOF
}

func main() {
    func1("Go")
}

5.4内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数
- close 用于管道通信
- len、cap len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
- new、make new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作 new() 是一个函数,不要忘记它的括号
- copy、append 用于复制和连接切片
- panic、recover 两者均用于错误处理机制
- print、println 底层打印函数,在部署环境中建议使用 fmt 包
- complex、real imag 用于创建和操作复数

5.5递归函数

最经典的例子便是计算斐波那契数列,即前两个数为1,从第三个数开始每个数均为前两个数之和。

package main

import "fmt"

func main() {
    result := 0
    for i := 0; i <= 10; i++ {
        result = fibonacci(i)
        fmt.Printf("fibonacci(%d) is: %d\n", i, result)
    }
}

func fibonacci(n int) (res int) {
    if n <= 1 {
        res = 1
    } else {
        res = fibonacci(n-1) + fibonacci(n-2)
    }
    return
}

5.6将函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

package main

import (
    "fmt"
)

func main() {
    callback(1, Add)
}

func Add(a, b int) {
    fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b)
}

func callback(y int, f func(int, int)) {
    f(y, 2) // this becomes Add(1, 2)
}

5.7 闭包

下面是一个计算从 1 到 1 百万整数的总和的匿名函数:

func() {
    sum := 0
    for i := 1; i <= 1e6; i++ {
        sum += i
    }
}()

defer 语句和匿名函数
匿名函数同样被称之为闭包
计算函数执行时间

start := time.Now()
longCalculation()
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)

通过内存缓存来提升性能
当在进行大量的计算时,提升性能最直接有效的一种方式就是避免重复计算。通过在内存中缓存和重复利用相同计算的结果,称之为内存缓存。

5.8数组与切片

数组是具有相同 唯一类型 的一组已编号且长度固定的数据项序列(这是一种同构的数据结构)
数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的
。数组的编译时值初始化是按照数组顺序完成的
元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。
声明的格式是:
var identifier [len]type

2种方式遍历
for i:=0; i < len(arr1); i++{
    arr1[i] = ...
}
for i,_:= range arr1 {
...
}

Go 语言中的数组是一种 值类型 也就是 =赋值就是拷贝

var arr1 = new([5]int)  //指针类型
    var arr2 [5]int  //值类型

这样的结果就是当把一个数组赋值给另一个时,需要在做一次数组内存的拷贝操作。

5.8.1数组常量

var arrAge = [5]int{18, 20, 15, 22, 16}
var arrLazy = [...]int{5, 6, 7, 8, 22} //从技术上说它们其实变化成了切片
var arrKeyValue = [5]string{3: "Chris", 4: "Ron"}//只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串 在这里数组长度同样可以写成 … 或者直接忽略。
几何点(或者数学向量)是一个使用数组的经典例子。为了简化代码通常使用一个别名:

type Vector3D [3]float32
var vec Vector3D

将数组传递给函数
把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:
- 传递数组的指针
- 使用数组的切片

5.8.2切片

切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型)。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内.切片是一个 长度可变的数组。
优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。
声明切片的格式是: var identifier []type(不需要说明长度)。
一个切片在未初始化之前默认为 nil,长度为 0。
切片的初始化格式是:var slice1 []type = arr1[start:end]
如果某个人写:var slice1 []type = arr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是 arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是:slice1 = &arr1。
arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。
arr1[:3] 和 arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第三个)。
一个由数字 1、2、3 组成的切片可以这么生成:s := [3]int{1,2,3}[:] 甚至更简单的 s := []int{1,2,3}。

s2 := s[:] 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。一个切片 s 可以这样扩展到它的大小上限:s = s[:cap(s)],如果再扩大的话就会导致运行时错误
注意 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!
将切片传递给函数var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])

用 make() 创建一个切片
var slice1 []type = make([]type, len)
make 的使用方式是:func make([]T, len, cap),其中 cap 是可选参数。
下面两种方法可以生成相同的切片:

make([]int, 50, 100)
new([100]int)[0:50]

new() 和 make() 的区别
- new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体(参见第 10 章);它相当于 &T{}。
- make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。
换言之,new 函数分配内存,make 函数初始化
bytes 包
类型 []byte 的切片十分常见 bytes 包和字符串包十分类似
Buffer 可以这样定义:var buffer bytes.Buffer。
var r *bytes.Buffer = new(bytes.Buffer)
func NewBuffer(buf []byte) *Buffer
创建一个 Buffer 对象并且用 buf 初始化好;NewBuffer 最好用在从 buf 读取的时候使用。
通过 buffer 串联字符串

var buffer bytes.Buffer
for {
    if s, ok := getNextString(); ok { //method getNextString() not shown here
        buffer.WriteString(s)
    } else {
        break
    }
}
fmt.Print(buffer.String(), "\n")

这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候。

5.8.3For-range 结构

这种构建方法可以应用于数组和切片:

for ix, value := range slice1 {
    ...
}

5.8.4切片重组(reslice)

slice1 := make([]type, start_length, capacity)
改变切片长度的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。

5.8.5字符串、数组和切片的应用

  • 从字符串生成字节切片
    可以通过代码 len([]int32(s)) 来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s) 效率会更高一点
  • 获取字符串的某一部分
    使用 substr := str[start:end] 可以从字符串 str 获取到从索引 start 开始到 end-1 位置的子字符串。同样的,str[start:] 则表示获取从 start 开始到 len(str)-1 位置的子字符串。而 str[:end] 表示获取从 0 开始到 end-1 的子字符串。
  • 字符串和切片的内存结构
    在内存中,一个字符串实际上是一个双字结构,即一个指向实际数据的指针和记录字符串长度的整数
  • 修改字符串中的某个字符
    Go 语言中的字符串是不可变的
    将切片 b 的元素追加到切片 a 之后:a = append(a, b…)
    复制切片 a 的元素到新的切片 b 上:

    b = make([]T, len(a))
    copy(b, a)
    删除位于索引 i 的元素:a = append(a[:i], a[i+1:]…)

  • 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]…)

  • 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)…)
  • 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]…)…)
  • 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], -
  • append(make([]T, j), a[i:]…)…)
  • 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, -a[i:]…)…)
  • 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]
  • 将元素 x 追加到切片 a:a = append(a, x)
    ###6 Map
    map 是一种特殊的数据结构:一种元素对(pair)的无序集合,pair 的一个元素是 key,对应的另一个元素是 value,所以这个结构也称为关联数组或字典。map 这种数据结构在其他编程语言中也称为字典(Python)、hash 和 HashTable 等

6.1声明、初始化和 make

map 是引用类型,可以使用如下声明:

var map1 map[keytype]valuetype
var map1 map[string]int

map 可以用 {key1: val1, key2: val2} 的描述方法来初始化,就像数组和结构体一样。
map 是 引用类型 的: 内存用 make 方法来分配。
map 的初始化:var map1 = make(map[keytype]valuetype)。
或者简写为:map1 := make(map[keytype]valuetype)。
不要使用 new,永远用 make 来构造 map
使用 func() int 作为值的 map:

package main
import "fmt"

func main() {
    mf := map[int]func() int{
        1: func() int { return 10 },
        2: func() int { return 20 },
        5: func() int { return 50 },
    }
    fmt.Println(mf)
}

输出结果为:map[1:0x10903be0 5:0x10903ba0 2:0x10903bc0]: 整形都被映射到函数地址。
用切片作为 map 的值

mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)

6.2 测试键值对是否存在及删除元素

val1, isPresent = map1[key1]
isPresent 返回一个 bool 值:如果 key1 存在于 map1,val1 就是 key1 对应的 value 值,并且 isPresent为true;如果 key1 不存在,val1 就是一个空值,并且 isPresent 会返回 false。

if _, ok := map1[key1]; ok {
    // ...
}

从 map1 中删除 key1: 直接 delete(map1, key1) 就可以。如果 key1 不存在,该操作不会产生错误。

6.3 for-range 的配套用法

可以使用 for 循环构造 map:

for key, value := range map1 {
    ...
}

如果只想获取 key,你可以这么使用:

for key := range map1 {
    fmt.Printf("key is: %d\n", key)
}

6.4map 类型的切片

假设我们想获取一个 map 类型的切片,我们必须使用两次 make() 函数,第一次分配切片,第二次分配 切片中每个 map 元素

package main
import "fmt"

func main() {
    // Version A:
    items := make([]map[int]int, 5)
    for i:= range items {
        items[i] = make(map[int]int, 1)
        items[i][1] = 2
    }
    fmt.Printf("Version A: Value of items: %v\n", items)

    // Version B: NOT GOOD!
    items2 := make([]map[int]int, 5)
    for _, item := range items2 {
        item = make(map[int]int, 1) // item is only a copy of the slice element.
        item[1] = 2 // This 'item' will be lost on the next iteration.
    }
    fmt.Printf("Version B: Value of items: %v\n", items2)
}

map 默认是无序的,不管是按照 key 还是按照 value 默认都不排序
将 map 的键值对调

7.结构(struct)与方法(method)

结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体
结构体也是值类型,因此可以通过 new 函数来创建
组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。

7.1 结构体定义

type identifier struct {
    field1 type1
    field2 type2
    ...
}

type T struct {a, b int} 也是合法的语法,它更适用于简单的结构体。
使用 new
var t *T = new(T)
如果需要可以把这条语句放在不同的行
声明 var t T 也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T
无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的 选择器符(selector-notation) 来引用结构体的字段:

type myStruct struct { i int }
var v myStruct    // v是结构体类型变量
var p *myStruct   // p是指向一个结构体类型变量的指针
v.i
p.i

初始化一个结构体实例(一个结构体字面量:struct-literal)的更简短和惯用的方式如下:

 ms := &struct1{10, 15.5, "Chris"}
    // 此时ms的类型是 *struct1
var ms struct1
    ms = struct1{10, 15.5, "Chris"}

&struct1{a, b, c} 是一种简写,底层仍然会调用 new (),
这里值的顺序必须按照字段顺序来写。
表达式 new(Type) 和 &Type{} 是等价的

type Interval struct {
    start int
    end   int
}
 //初始化方式:
intr := Interval{0, 3}            (A)
intr := Interval{end:5, start:1}  (B)
intr := Interval{end:5}           (C)

如果想知道结构体类型T的一个实例占用了多少内存,可以使用:size := unsafe.Sizeof(T{})

7.2 map 和 struct vs new() 和 make()

现在为止我们已经见到了可以使用 make() 的三种类型中的其中两个
slices / maps / channels
试图 make() 一个结构体变量,会引发一个编译错误

7.3带标签的结构体

结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记

package main

import (
    "fmt"
    "reflect"
)

type TagType struct { // tags
    field1 bool   "An important answer"
    field2 string "The name of the thing"
    field3 int    "How much there are"
}

func main() {
    tt := TagType{true, "Barak Obama", 1}
    for i := 0; i < 3; i++ {
        refTag(tt, i)
    }
}

func refTag(tt TagType, ix int) {
    ttType := reflect.TypeOf(tt)
    ixField := ttType.Field(ix)
    fmt.Printf("%v\n", ixField.Tag)
}

7.4 匿名字段和内嵌结构体

结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。
可以粗略地将这个和面向对象语言中的继承概念相比较,随后将会看到它被用来模拟类似继承的行为

package main

import "fmt"

type innerS struct {
    in1 int
    in2 int
}

type outerS struct {
    b    int
    c    float32
    int  // anonymous field
    innerS //anonymous field
}

func main() {
    outer := new(outerS)
    outer.b = 6
    outer.c = 7.5
    outer.int = 60
    outer.in1 = 5
    outer.in2 = 10

    fmt.Printf("outer.b is: %d\n", outer.b)
    fmt.Printf("outer.c is: %f\n", outer.c)
    fmt.Printf("outer.int is: %d\n", outer.int)
    fmt.Printf("outer.in1 is: %d\n", outer.in1)
    fmt.Printf("outer.in2 is: %d\n", outer.in2)

    // 使用结构体字面量
    outer2 := outerS{6, 7.5, 60, innerS{5, 10}}
    fmt.Println("outer2 is:", outer2)
}

在一个结构体中对于每一种数据类型只能有一个匿名字段。
内嵌结构体
同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。
命名冲突
当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?
- 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式;
- 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。

7.5方法

在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 中有一个概念,它和方法有着同样的名字,并且大体上意思相同:Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。

一个类型加上它的方法等价于面向对象中的一个类。一个重要的区别是:在 Go 中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在在不同的源文件,唯一的要求是:它们必须是同一个包的。
类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。
不允许方法重载,即对于一个类型只能有一个给定名称的方法
有同样名字的方法可以在 2 个或多个不同的接收者类型上存在,比如在同一个包里这么做是允许的:

func (a *denseMatrix) Add(b Matrix) Matrix
func (a *sparseMatrix) Add(b Matrix) Matrix

别名类型不能有它原始类型上已经定义过的方法。
定义方法的一般格式如下:
func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
如果 recv 一个指针,Go 会自动解引用。如果方法不需要使用 recv 的值,可以用 _ 替换它,
recv 就像是面向对象语言中的 this 或 self,
下面是非结构体类型上方法的例子

package main

import "fmt"

type IntVector []int

func (v IntVector) Sum() (s int) {
    for _, x := range v {
        s += x
    }
    return
}

func main() {
    fmt.Println(IntVector{1, 2, 3}.Sum()) // 输出是6
}

* 函数和方法的区别*
函数将变量作为参数:Function1(recv)
方法在变量上被调用:recv.Method1()
receiver_type 叫做 (接收者)基本类型,这个类型必须在和方法同样的包中被声明。
方法没有和数据定义(结构体)混在一起:它们是正交的类型;表示(数据)和行为(方法)是独立的。
指针或值作为接收者
鉴于性能的原因,recv 最常见的是一个指向 receiver_type 的指针(因为我们不想要一个实例的拷贝,如果按值调用的话就会是这样),特别是在 receiver 类型是结构体时,就更是如此了。

在值和指针上调用方法:
可以有连接到类型的方法,也可以有连接到类型指针的方法。
但是这没关系:对于类型 T,如果在 *T 上存在方法 Meth(),并且 t 是这个类型的变量,那么 t.Meth() 会被自动转换为 (&t).Meth()。
指针方法和值方法都可以在指针或非指针上被调用

7.6方法和未导出字段

提供 getter 和 setter 方法。对于 setter 方法使用 Set 前缀,对于 getter 方法只使用成员名。

package person

type Person struct {
    firstName string
    lastName  string
}

func (p *Person) FirstName() string {
    return p.firstName
}

func (p *Person) SetFirstName(newName string) {
    p.firstName = newName
}

可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。

package main

import (
    "fmt"
    "math"
)

type Point struct {
    x, y float64
}

func (p *Point) Abs() float64 {
    return math.Sqrt(p.x*p.x + p.y*p.y)
}

type NamedPoint struct {
    Point
    name string
}

func (n *NamedPoint) Abs() float64 {
    return n.Point.Abs() * 100.
}
func main() {
    n := &NamedPoint{Point{3, 4}, "Pythagoras"}

    fmt.Println(n.Abs()) // 打印5
}

因为一个结构体可以嵌入多个匿名类型,所以实际上我们可以有一个简单版本的多重继承
结构体内嵌和自己在同一个包中的结构体时,可以彼此访问对方所有的字段和方法。

7.7多重继承

通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。

package main

import (
    "fmt"
)

type Camera struct{}

func (c *Camera) TakeAPicture() string {
    return "Click"
}

type Phone struct{}

func (p *Phone) Call() string {
    return "Ring Ring"
}

type CameraPhone struct {
    Camera
    Phone
}

func main() {
    cp := new(CameraPhone)
    fmt.Println("Our new CameraPhone exhibits multiple behaviors...")
    fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture())
    fmt.Println("It works like a Phone too: ", cp.Call())
}

7.8垃圾回收和 SetFinalizer

通过调用 runtime.GC() 函数可以显式的触发 GC,但这只在某些罕见的场景下才有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立即释放一大片内存,
如果想知道当前的内存状态,可以使用:

// fmt.Printf(“%d\n”, runtime.MemStats.Alloc/1024)
// 此处代码在 Go 1.5.1下不再有效,更正为
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf(“%d Kb\n”, m.Alloc / 1024)

8.0接口(Interfaces)与反射(reflection)

接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
通过如下格式定义接口:

type Namer interface {
    Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

(按照约定,只包含一个方法的)接口的名字由方法名加 [e]r 后缀组成,例如 Printer、Reader、Writer、Logger、Converter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NET 或 Java 中那样)。

8.1类型断言

一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型
v := varI.(T) // unchecked type assertion
varI 必须是一个接口变量,否则编译器会报错
更安全的方式是使用以下形式来进行类型断言:

if v, ok := varI.(T); ok {  // checked type assertion
    Process(v)
    return
}

8.2类型判断:type-switch

switch t := areaIntf.(type) {
case *Square:
    fmt.Printf("Type Square %T with value %v\n", t, t)
case *Circle:
    fmt.Printf("Type Circle %T with value %v\n", t, t)
case nil:
    fmt.Printf("nil value: nothing to check?\n")
default:
    fmt.Printf("Unexpected type %T\n", t)
}

可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough 。

switch areaIntf.(type) {
case *Square:
    // TODO
case *Circle:
    // TODO
...
default:
    // TODO
}

8.3测试一个值是否实现了某个接口

type Stringer interface {
    String() string
}

if sv, ok := v.(Stringer); ok {
    fmt.Printf("v implements String(): %s\n", sv.String()) // note: sv, not v
}

使用接口使代码更具有普适性。

8.4使用方法集与接口

作用于变量上的方法实际上是不区分变量到底是指针还是值的
指针调用值类型方法时候会 指针会被自动解引用
在接口上调用方法时,必须有和方法定义时相同的接收者类型或者是可以从具体类型 P 直接可以辨识的:
- 指针方法可以通过指针调用
- 值方法可以通过值调用
- 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
- 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

Go 语言规范定义了接口方法集的调用规则:
- 类型 T 的可调用方法集包含接受者为 T 或 T 的所有方法集
- 类型 T 的可调用方法集包含接受者为 T 的所有方法
- 类型 T 的可调用方法集不包含接受者为 *T 的方法

8.5空接口

接口或者最小接口 不包含任何方法,它对实现不做任何要求:
type Any interface {}
任何其他类型都实现了空接口(它不仅仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。
每个 interface {} 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。

  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值