Golang基础

包的导入

单行导入与多行导入

在 Go 语言中,一个包可包含多个 .go 文件(这些文件必须得在同一级文件夹中),只要这些 .go 文件的头部都使用 package 关键字声明了同一个包。

导入包主要可分为两种方式:

  • 单行导入
import "fmt"
import "sync"
  • 多行导入
import(
    "fmt"
    "sync"
)

使用别名

在一些场景下,我们可能需要对导入的包进行重新命名,比如

  • 我们导入了两个具有同一包名的包时产生冲突,此时这里为其中一个包定义别名
import (
    "crypto/rand"
    mrand "math/rand" // 将名称替换为mrand避免冲突
)
  • 我们导入了一个名字很长的包,为了避免后面都写这么长串的包名,可以这样定义别名
import hw "helloworldtestmodule"
  • 防止导入的包名和本地的变量发生冲突,比如 path 这个很常用的变量名和导入的标准包冲突。
import pathpkg "path"

使用点操作

如里在我们程序内部里频繁使用了一个工具包,比如 fmt,那每次使用它的打印函数打印时,都要 包名+方法名。

对于这种使用高频的包,可以在导入的时,就把它定义会 “自己人”(方法是使用一个 . ),自己人的话,不分彼此,它的方法,就是我们的方法。

从此,我们打印再也不用加 fmt 了。

import . "fmt"

func main() {
    Println("hello, world")
}

但这种用法,会有一定的隐患,就是导入的包里可能有函数,会和我们自己的函数发生冲突。

包的初始化

每个包都允许有一个 init 函数,当这个包被导入时,会执行该包的这个 init 函数,做一些初始化任务。

对于 init 函数的执行有几点需要注意

  1. init 函数优先于 main 函数执行

  2. 在一个包引用链中,包的初始化是深度优先的。比如,有这样一个包引用关系:main→A→B→C,那么初始化顺序为

    C.init→B.init→A.init→main
    
  3. 同一个包甚至同一个源文件,可以有多个 init 函数

  4. init 函数不能有入参和返回值

  5. init 函数不能被其他函数调用

  6. 同一个包内的多个 init 顺序是不受保证的

  7. 在 init 之前,其实会先初始化包作用域的常量和变量(常量优先于变量),具体可参考如下代码

    package main
    
    import "fmt"
    
    func init()  {
     fmt.Println("init1:", a)
    }
    
    func init()  {
     fmt.Println("init2:", a)
    }
    
    var a = 10
    const b = 100
    
    func main() {
     fmt.Println("main:", a)
    }
    // 执行结果
    // init1: 10
    // init2: 10
    // main: 10
    

包的匿名导入

当我们导入一个包时,如果这个包没有被使用到,在编译时,是会报错的。

但是有些情况下,我们导入一个包,只想执行包里的 init 函数,来运行一些初始化任务,此时怎么办呢?

可以使用匿名导入,用法如下,其中下划线为空白标识符,并不能被访问

// 注册一个PNG decoder
import _ "image/png"

由于导入时,会执行 init 函数,所以编译时,仍然会将这个包编译到可执行文件中。

导入的是路径还是包?

当我们使用 import 导入 testmodule/foo 时,初学者,经常会问,这个 foo 到底是一个包呢,还是只是包所在目录名?

import "testmodule/foo"

为了得出这个结论,专门做了个试验(请看「第七点里的代码示例」),最后得出的结论是:

  • 导入时,是按照目录导入。导入目录后,可以使用这个目录下的所有包。
  • 出于习惯,包名和目录名通常会设置成一样,所以会让你有一种你导入的是包的错觉。

相对导入和绝对导入

据我了解在 Go 1.10 之前,好像是不支持相对导入的,在 Go 1.10 之后才可以。

绝对导入:从 $GOPATH/src$GOROOT 或者 $GOPATH/pkg/mod 目录下搜索包并导入

相对导入:从当前目录中搜索包并开始导入。就像下面这样

import (
    "./module1"
    "../module2"
    "../../module3"
    "../module4/module5"
)
  • Go Modules 不支持相对导入,在你开启 GO111MODULE 后,无法使用相对导入。

最后,不得不说的是:使用相对导入的方式,项目可读性会大打折扣,不利用开发者理清整个引用关系。

所以一般更推荐使用绝对引用的方式。使用绝对引用的话,又要谈及优先级了

包导入路径优先级

前面一节,介绍了三种不同的包依赖管理方案,不同的管理模式,存放包的路径可能都不一样,有的可以将包放在 GOPATH 下,有的可以将包放在 vendor 下,还有些包是内置包放在 GOROOT 下。

那么问题就来了,如果在这三个不同的路径下,有一个相同包名但是版本不同的包,我们导入的时候,是选择哪个进行导入呢?

这就需要我们搞懂,在 Golang 中包搜索路径优先级是怎样的?

这时候就需要区分,是使用哪种模式进行包的管理的。

如果使用 govendor

当我们导入一个包时,它会:

  1. 先从项目根目录的 vendor 目录中查找
  2. 最后从 $GOROOT/src 目录下查找
  3. 然后从 $GOPATH/src 目录下查找
  4. 都找不到的话,就报错。

为了验证这个过程,我在创建中创建一个 vendor 目录后,就开启了 vendor 模式了,我在 main.go 中随便导入一个包 pkg,由于这个包是我随便指定的,当然会找不到,找不到就会报错, Golang 会在报错信息中打印中搜索的过程,从这个信息中,就可以看到 Golang 的包查找优先级了。

Golang基础

如果使用 go modules

你导入的包如果有域名,都会先在 $GOPATH/pkg/mod 下查找,找不到就连网去该网站上寻找,找不到或者找到的不是一个包,则报错。

而如果你导入的包没有域名(比如 “fmt”这种),就只会到 $GOROOT 里查找。

还有一点很重要,当你的项目下有 vendor 目录时,不管你的包有没有域名,都只会在 vendor 目录中想找。

Golang基础

通常vendor 目录是通过 go mod vendor 命令生成的,这个命令会将项目依赖全部打包到你的项目目录下的 verdor 文件夹中。

fmt库的使用

fmt包实现了类似C语言printf和scanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。

Print

Print系列函数会将内容输出到系统的标准输出,

  1. 区别在于Print函数直接输出内容,
  2. Printf函数支持格式化输出字符串,
  3. Println函数会在输出内容的结尾添加一个换行符,逗号之间会有空格。
func main() {
    fmt.Print("hello", "world\n")
    fmt.Println("hello", "world")
    fmt.Printf("hello world\n")
}

输出如下

helloworld
hello world
hello world

Fprint

Fprint系列函数会将内容输出到一个io.Writer接口类型的变量w中,我们通常用这个函数往文件中写入内容。

func Fprint(w io.Writer, a ...interface{}) (n int, err error)
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)

举个例子:

// 向标准输出写入内容
fmt.Fprintln(os.Stdout, "向标准输出写入内容")
fileObj, err := os.OpenFile("./xx.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
    fmt.Println("打开文件出错,err:", err)
    return
}
name := "枯藤"
// 向打开的文件句柄中写入内容
fmt.Fprintf(fileObj, "往文件中写如信息:%s", name)

注意,只要满足io.Writer接口的类型都支持写入。

Sprint

Sprint系列函数会把传入的数据生成并返回一个字符串。

func Sprint(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string
func Sprintln(a ...interface{}) string

简单的示例代码如下:

s1 := fmt.Sprint("枯藤")
name := "枯藤"
age := 18
s2 := fmt.Sprintf("name:%s,age:%d", name, age)
s3 := fmt.Sprintln("枯藤")
fmt.Println(s1, s2, s3)

Printf占位符

通用占位符

  • 占位符 说明
    %v 值的默认格式表示
    %+v 类似%v,但输出结构体时会添加字段名
    %#v 值的Go语法表示
    %T 打印值的类型
    %% 百分号
type Profile struct {
    name string
    gender string
    age int
}

func main() {
    var people = Profile{name:"xxx", gender: "male", age:18}
    fmt.Printf("%v \n", people)  // output: {xxx male 18}
    fmt.Printf("%T \n", people)  // output: main.Profile

    // 打印结构体名和类型
    fmt.Printf("%#v \n", people) // output: main.Profile{name:"xxx", gender:"male", age:18}
    fmt.Printf("%+v \n", people) // output: {name:xxx gender:male age:18}
    fmt.Printf("%% \n") // output: %
}

布尔型

占位符 说明
%t true或false
func main() {
    fmt.Printf("%t \n", true)   //output: true
    fmt.Printf("%t \n", false)  //output: false
}

字符串和[]byte

  • 占位符 说明
    %s 直接输出字符串或者[]byte
    %q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
    %x 每个字节用两字符十六进制数表示(使用a-f
    %X 每个字节用两字符十六进制数表示(使用A-F)
func main() {
    fmt.Printf("%s \n", []byte("Hello, Golang"))  // output: Hello, Golang
    fmt.Printf("%s \n", "Hello, Golang")     // output: Hello, Golang

    fmt.Printf("%q \n", []byte("Hello, Golang"))  // output: "Hello, Golang"
    fmt.Printf("%q \n", "Hello, Golang")     // output: "Hello, Golang"
    fmt.Printf("%q \n", `hello \r\n world`)  // output: "hello \\r\\n world"

    fmt.Printf("%x \n", "Hello, Golang")     // output: 48656c6c6f2c20476f6c616e67
    fmt.Printf("%X \n", "Hello, Golang")     // output: 48656c6c6f2c20476f6c616e67
}

指针

占位符 说明
%p 表示为十六进制,并加上前导的0x
func main() {
    var people = Profile{name:"xxx", gender: "male", age:18}
    fmt.Printf("%p", &people)  // output: 0xc0000a6150
}

整型

  • 占位符 说明
    %b 表示为二进制
    %c 该值对应的unicode码值
    %d 表示为十进制
    %o 表示为八进制
    %x 表示为十六进制,使用a-f
    %X 表示为十六进制,使用A-F
    %U 表示为Unicode格式:U+1234,等价于”U+%04X”
    %q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
func main() {
    n := 1024
    fmt.Printf("%d 的 2 进制:%b \n", n, n)
    fmt.Printf("%d 的 8 进制:%o \n", n, n)
    fmt.Printf("%d 的 10 进制:%d \n", n, n)
    fmt.Printf("%d 的 16 进制:%x \n", n, n)

    // 将 10 进制的整型转成 16 进制打印: %x 为小写, %X 为小写
    fmt.Printf("%x \n", 1024)
    fmt.Printf("%X \n", 1024)

    // 根据 Unicode码值打印字符
    fmt.Printf("ASCII 编码为%d 表示的字符是: %c \n", 65, 65)  // output: A

    // 根据 Unicode 编码打印字符
    fmt.Printf("%c \n", 0x4E2D)  // output: 中
    // 打印 raw 字符时
    fmt.Printf("%q \n", 0x4E2D)  // output: ‘中‘

    // 打印 Unicode 编码
    fmt.Printf("%U \n", ‘中‘)   // output: U+4E2D
}

运行后,输出如下

1024 的 2 进制:10000000000
1024 的 8 进制:2000
1024 的 10 进制:1024
1024 的 16 进制:400
400
400
ASCII 编码为65 表示的字符是: A
中
‘中‘
U+4E2D

浮点数与复数

占位符 说明
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
func main() {
    f := 12.34
    fmt.Printf("%b\n", f)
    fmt.Printf("%e\n", f)
    fmt.Printf("%E\n", f)
    fmt.Printf("%f\n", f)
    fmt.Printf("%g\n", f)
    fmt.Printf("%G\n", f)
}

输出如下

6946802425218990p-49
1.234000e+01
1.234000E+01
12.340000
12.34
12.34

宽度标识符

宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。

浮点数

占位符 说明
%f 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0
func main() {
    n := 12.34
    fmt.Printf("%f\n", n)     // 以默认精度打印
    fmt.Printf("%9f\n", n)   // 宽度为9,默认精度
    fmt.Printf("%.2f\n", n)  // 默认宽度,精度2
    fmt.Printf("%9.2f\n", n)  //宽度9,精度2
    fmt.Printf("%9.f\n", n)    // 宽度9,精度0
}

输出如下

10.240000
10.240000
10.24
    10.24
       10

字符串

func main() {
    // 打印的值宽度为5,若不足5个字符,则在前面补空格凑足5个字符。
    fmt.Printf("a%5sc\n", "b")   // output: a    bc
    // 打印的值宽度为5,若不足5个字符,则在后面补空格凑足5个字符。
    fmt.Printf("a%-5sc\n", "b")  //output: ab    c

    // 不想用空格补全,还可以指定0,其他数值不可以,注意:只能在前边补全,后边补全无法指定字符
    fmt.Printf("a%05sc\n", "b") // output: a0000bc
     // 若超过5个字符,不会截断
    fmt.Printf("a%5sd\n", "bbbccc") // output: abbbcccd
}

输出如下

a    bc
ab    c
a0000bc
abbbcccd

%+

  • %+v:若值为结构体,则输出将包括结构体的字段名。
  • %+q:保证只输出ASCII编码的字符,非 ASCII 字符则以unicode编码表示
func main() {
    // 若值为结构体,则输出将包括结构体的字段名。
    var people = Profile{name:"xxx", gender: "male", age:18}
    fmt.Printf("%v \n", people) // output: {xxx male 18}
    fmt.Printf("%+v \n", people) // output: {name:xxx gender:male age:18}

    // 保证只输出ASCII编码的字符
    fmt.Printf("%q \n", "golang")  // output: "golang"
    fmt.Printf("%+q \n", "golang")  // output: "golang"

    // 非 ASCII 字符则以unicode编码表示
    fmt.Printf("%q \n", "中文")  // output: "中文"
    fmt.Printf("%+q \n", "中文") // output: "\u4e2d\u6587"
}

输出如下

{xxx male 18}
{name:xxx gender:male age:18}

"golang"
"golang"

"中文"
"\u4e2d\u6587"

%#

  • %#x:给打印出来的是 16 进制字符串加前缀 0x
  • %#q:用反引号包含,打印原始字符串
  • %#U:若是可打印的字符,则将其打印出来
  • %#p:若是打印指针的内存地址,则去掉前缀 0x
func main() {
    // 对于打印出来的是 16 进制,则加前缀 0x
    fmt.Printf("%x \n", "Hello, Golang")     // output: 48656c6c6f2c20476f6c616e67
    fmt.Printf("%#x \n", "Hello, Golang")     // output: 0x48656c6c6f2c20476f6c616e67

    // 用反引号包含,打印原始字符串
    fmt.Printf("%q \n", "Hello, Golang")     // output: "Hello, Golang"
    fmt.Printf("%#q \n", "Hello, Golang")     // output: `Hello, Golang`

    // 若是可打印的字符,则将其打印出来
    fmt.Printf("%U \n", ‘中‘)     // output: U+4E2D
    fmt.Printf("%#U \n", ‘中‘)     // output: U+4E2D ‘中‘

    // 若是打印指针的内存地址,则去掉前缀 0x
    a := 1024
    fmt.Printf("%p \n", &a)  // output: 0xc0000160e0
    fmt.Printf("%#p \n", &a)  // output: c0000160e0
}

正负号

如果是正数,则留一个空格,表示正数

如果是负数,则在此位置,用 - 表示

func main() {
    fmt.Printf("1% d3\n", 22)
    fmt.Printf("1% d3\n", -22)
}

输出如下

1 223
1-223

Scan

Go语言fmt包下有fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

函数定签名如下:

func Scan(a ...interface{}) (n int, err error)
  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。

  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。

  • 具体代码示例如下:

    func main() {
        var (
            name    string
            age     int
            married bool
        )
        fmt.Scan(&name, &age, &married)
        fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
    }
    

Scanf

函数签名如下:

func Scanf(format string, a ...interface{}) (n int, err error)
  • Scanf从标准输入扫描文本,根据format参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

代码示例如下:

func main() {
    var (
        name    string
        age     int
        married bool
    )
    fmt.Scanf("1:%s 2:%d 3:%t", &name, &age, &married)
    fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
}

Scanln

函数签名如下:

func Scanln(a ...interface{}) (n int, err error)
  • Scanln类似Scan,它在遇到换行时才停止扫描。最后一个数据后面必须有换行或者到达结束位置。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。

具体代码示例如下:

    func main() {
        var (
            name    string
            age     int
            married bool
        )
        fmt.Scanln(&name, &age, &married)
        fmt.Printf("扫描结果 name:%s age:%d married:%t \n", name, age, married)
    }

fmt.Scanln遇到回车就结束扫描了,这个比较常用。

bufio.NewReader

有时候我们想完整获取输入的内容,而输入的内容可能包含空格,这种情况下可以使用bufio包来实现。示例代码如下:

func bufioDemo() {
    reader := bufio.NewReader(os.Stdin) // 从标准输入生成读对象
    fmt.Print("请输入内容:")
    text, _ := reader.ReadString(‘\n‘) // 读到换行
    text = strings.TrimSpace(text)
    fmt.Printf("%#v\n", text)
}

Fscan系列

这几个函数功能分别类似于fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从io.Reader中读取数据。

func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)

Sscan系列

这几个函数功能分别类似于fmt.Scan、fmt.Scanf、fmt.Scanln三个函数,只不过它们不是从标准输入中读取数据而是从指定字符串中读取数据。

func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)

变量

声明

前面两种可用于定义常量,只需把关键字 var 变成 const 即可。

第一种 :一行声明一个变量

var <name> <type>

其中 var 是关键字(固定不变),name 是变量名,type 是类型。

使用 var ,虽然只指定了类型,但是 Go 会对其进行隐式初始化,比如 string 类型就初始化为空字符串,int 类型就初始化为0,float 就初始化为 0.0,bool类型就初始化为false,指针类型就初始化为 nil。

若想在声明过程,顺便也初始化,可以这样写

var name string = "Go"

在 Go 文件中的完整代码如下,为了不写重复性的代码,后续不再貼完整代码,只貼关键代码

package main

import "fmt"

func main()  {
    var name string = "Go"
    fmt.Println(name)
}

从右值(等号右边的值,value)来看,明显是个 string 类型,因此也可以将其简化为

var name = "Go"

若你的右值带有小数点,在不指定类型的情况下,编译器会将你的这个变量声明为 float64,但是很多情况下,我们并不需要这么高的精度(占用的内存空间更大)

这种情况下,推荐指定类型,不要偷懒

var rate float32 = 0.89

第二种:多个变量一起声明

声明多个变量,除了可以按照上面写成多行之外,还可以写成下面这样

var (
    name string
    age int
    gender string
)

第三种:声明和初始化一个变量

使用 := (推导声明写法或者短类型声明法:编译器会自动根据右值类型推断出左值的对应类型。),可以声明一个变量,并对其进行(显式)初始化。

name := "Go"

// 等价于

var name string = "Go"

// 等价于

var name = "Go"

但这种方法有个限制就是,只能用于函数内部

第四种:声明和初始化多个变量

name, age := "wangbm", 28

这种方法,也经常用于变量的交换

var a int = 100
var b int = 200
b, a = a, b

第五种:new 函数声明一个指针变量

在这里要先讲一下,指针的相关内容。

变量分为两种 普通变量指针变量

普通变量,存放的是数据本身,而指针变量存放的是数据的地址。

如下代码,age 是一个普通变量,存放的内容是 28,而 ptr 是 存放变量age值的内存地址:0xc000010098

package main

import "fmt"

func main()  {
    var age int = 28
    var ptr = &age  // &后面接变量名,表示取出该变量的内存地址
    fmt.Println("age: ", age)
    fmt.Println("ptr: ", ptr)
}

输出

age:  28
ptr:  0xc000010098

而这里要说的 new 函数,是 Go 里的一个内建函数。

使用表达式 new(Type) 将创建一个Type类型的匿名变量,初始化为Type类型的零值,然后返回变量地址,返回的指针类型为*Type

package main

import "fmt"

func main()  {
    ptr := new(int)
    fmt.Println("ptr address: ", ptr)
    fmt.Println("ptr value: ", *ptr)  // * 后面接指针变量,表示从内存地址中取出值
}

输出

ptr address:  0xc000010098
ptr value:  0

用new创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用new(Type)。换言之,new函数类似是一种语法糖,而不是一个新的基础概念。

如下两种写法,可以说是等价的

// 使用 new
func newInt() *int {
    return new(int)
}

// 使用传统的方式
func newInt() *int {
    var dummy int
    return &dummy
}

以上不管哪种方法,变量/常量都只能声明一次,声明多次,编译就会报错。

匿名变量

但也有例外,这就要说到一个特殊变量:匿名变量,也称作占位符,或者空白标识符,用下划线表示。

匿名变量,优点有三:

  • 不分配内存,不占用内存空间
  • 不需要你为命名无用的变量名而纠结
  • 多次声明不会有任何问题

通常我们用匿名接收必须接收,但是又不会用到的值。

func GetData() (int, int) {
    return 100, 200
}
func main(){
    a, _ := GetData()
    _, b := GetData()
    fmt.Println(a, b)
}

数据类型

GO语言中绝大部分类型都是基于值语义(值传递),包括:

  1. 基本类型:如byte、int、bool、float32、float64和string等;
  2. 复杂类型:如数组(array)、结构体(struct)、指针(pointer)等

GO语言中有下面四个类型比较特别,看起来像引用类型,为地址传递

  1. 数组切片:指向数组(array)的一个区间
  2. map:极其常见的数据结构,提供键值查询的能力
  3. channel:执行体(goroutine)间提供的通信设施
  4. 接口(interface):对一组满足某个契约的类型的抽象

整型

Go 语言中,整数类型可以再细分成10个类型,为了方便大家学习,我将这些类型整理成一张表格。

Golang基础

int 和 uint 的区别就在于一个 u,有 u 说明是无符号,没有 u 代表有符号。

解释这个符号的区别

int8uint8 举例,8 代表 8个bit,能表示的数值个数有 2^8 = 256。

uint8 是无符号,能表示的都是正数,0-255,刚好256个数。

int8 是有符号,既可以正数,也可以负数,那怎么办?对半分呗,-128-127,也刚好 256个数。

int8 int16 int32 int64 这几个类型的最后都有一个数值,这表明了它们能表示的数值个数是固定的。

而 int 并没有指定它的位数,说明它的大小,是可以变化的,那根据什么变化呢?

  • 当你在32位的系统下,int 和 uint 都占用 4个字节,也就是32位。
  • 若你在64位的系统下,int 和 uint 都占用 8个字节,也就是64位。

出于这个原因,在某些场景下,你应当避免使用 int 和 uint ,而使用更加精确的 int32 和 int64,比如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)

不同进制的表示方法

出于习惯,在初始化数据类型为整型的变量时,我们会使用10进制的表示法,因为它最直观,比如这样,表示整数10.

var num int = 10

不过,你要清楚,你一样可以使用其他进制来表示一个整数,这里以比较常用的2进制、8进制和16进制举例。

2进制:以0b0B为前缀

var num01 int = 0b1100

8进制:以0o或者 0O为前缀

var num02 int = 0o14

16进制:以0x 为前缀

var num03 int = 0xC

下面用一段代码分别使用二进制、8进制、16进制来表示 10 进制的数值:12

package main

import (
    "fmt"
)

func main() {
    var num01 int = 0b1100
    var num02 int = 0o14
    var num03 int = 0xC

    fmt.Printf("2进制数 %b 表示的是: %d \n", num01, num01)
    fmt.Printf("8进制数 %o 表示的是: %d \n", num02, num02)
    fmt.Printf("16进制数 %X 表示的是: %d \n", num03, num03)
}

输出如下

2进制数 1100 表示的是: 12
8进制数 14 表示的是: 12
16进制数 C 表示的是: 12

以上代码用过了 fmt 包的格式化功能,你可以参考这里去看上面的代码

%b    表示为二进制
%c    该值对应的unicode码值
%d    表示为十进制
%o    表示为八进制
%q    该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x    表示为十六进制,使用a-f
%X    表示为十六进制,使用A-F
%U    表示为Unicode格式:U+1234,等价于"U+%04X"
%E    用科学计数法表示
%f    用浮点数表示

浮点数

浮点数类型的值一般由整数部分、小数点“.”和小数部分组成。

其中,整数部分和小数部分均由10进制表示法表示。不过还有另一种表示方法。那就是在其中加入指数部分。指数部分由“E”或“e”以及一个带正负号的10进制数组成。比如,3.7E-2表示浮点数0.037。又比如,3.7E+1表示浮点数37

有时候,浮点数类型值的表示也可以被简化。比如,37.0可以被简化为37。又比如,0.037可以被简化为.037

有一点需要注意,在Go语言里,浮点数的相关部分只能由10进制表示法表示,而不能由8进制表示法或16进制表示法表示。比如,03.7表示的一定是浮点数3.7

float32 和 float64

Go语言中提供了两种精度的浮点数 float32 和 float64。

float32,也即我们常说的单精度,存储占用4个字节,也即4*8=32位,其中1位用来符号,8位用来指数,剩下的23位表示尾数

Golang基础

img

float64,也即我们熟悉的双精度,存储占用8个字节,也即8*8=64位,其中1位用来符号,11位用来指数,剩下的52位表示尾数

Golang基础

img

那么精度是什么意思?有效位有多少位?

精度主要取决于尾数部分的位数。

对于 float32(单精度)来说,表示尾数的为23位,除去全部为0的情况以外,最小为2-23,约等于1.19*10-7,所以float小数部分只能精确到后面6位,加上小数点前的一位,即有效数字为7位。

同理 float64(单精度)的尾数部分为 52位,最小为2-52,约为2.22*10-16,所以精确到小数点后15位,加上小数点前的一位,有效位数为16位。

通过以上,可以总结出以下几点:

一、float32 和 float64 可以表示的数值很多

浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

  • 常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38;
  • 常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308;
  • float32 和 float64 能表示的最小值分别为 1.4e-45 和 4.9e-324。

二、数值很大但精度有限

人家虽然能表示的数值很大,但精度位却没有那么大。

  • float32的精度只能提供大约6个十进制数(表示后科学计数法后,小数点后6位)的精度
  • float64的精度能提供大约15个十进制数(表示后科学计数法后,小数点后15位)的精度

这里的精度是什么意思呢?

比如 10000018这个数,用 float32 的类型来表示的话,由于其有效位是7位,将10000018 表示成科学计数法,就是 1.0000018 * 10^7,能精确到小数点后面6位。

此时用科学计数法表示后,小数点后有7位,刚刚满足我们的精度要求,意思是什么呢?此时你对这个数进行+1或者-1等数学运算,都能保证计算结果是精确的

import "fmt"
var myfloat float32 = 10000018
func main()  {
    fmt.Println("myfloat: ", myfloat)
    fmt.Println("myfloat: ", myfloat+1)
}

输出如下

myfloat:  1.0000018e+07
myfloat:  1.0000019e+07

上面举了一个刚好满足精度要求数据的临界情况,为了做对比,下面也举一个刚好不满足精度要求的例子。只要给这个数值多加一位数就行了。

换成 100000187,同样使用 float32类型,表示成科学计数法,由于精度有限,表示的时候小数点后面7位是准确的,但若是对其进行数学运算,由于第八位无法表示,所以运算后第七位的值,就会变得不精确。

这里我们写个代码来验证一下,按照我们的理解下面 myfloat01 = 100000182 ,对其+5 操作后,应该等于 myfloat02 = 100000187,

import "fmt"

var myfloat01 float32 = 100000182
var myfloat02 float32 = 100000187

func main() {
    fmt.Println("myfloat: ", myfloat01)
    fmt.Println("myfloat: ", myfloat01+5)
    fmt.Println(myfloat02 == myfloat01+5)
}

但是由于其类型是 float32,精度不足,导致最后比较的结果是不相等(从小数点后第七位开始不精确)

myfloat:  1.00000184e+08
myfloat:  1.0000019e+08
false

由于精度的问题,就会出现这种很怪异的现象,myfloat == myfloat +1 会返回 true

byte与rune

byte,占用1个节字,就 8 个比特位(2^8 = 256,因此 byte 的表示范围 0->255),所以它和 uint8 类型本质上没有区别,它表示的是 ACSII 表中的一个字符。

如下这段代码,分别定义了 byte 类型和 uint8 类型的变量 a 和 b

import "fmt"

func main() {
    var a byte = 65
    // 8进制写法: var a byte = ‘\101‘     其中 \ 是固定前缀
    // 16进制写法: var a byte = ‘\x41‘    其中 \x 是固定前缀

    var b uint8 = 66
    fmt.Printf("a 的值: %c \nb 的值: %c", a, b)

    // 或者使用 string 函数
    // fmt.Println("a 的值: ", string(a)," \nb 的值: ", string(b))
}

fmt.Printf 中的 %c 表示输入为单个字符

在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 ,字母 B 的ASCII 编号为 66,所以上面的代码也可以写成这样

import "fmt"

func main() {
    var a byte = ‘A‘
    var b uint8 = ‘B‘
    fmt.Printf("a 的值: %c \nb 的值: %c", a, b)
}

他们的输出结果都是一样的。

a 的值: A
b 的值: B

rune,占用4个字节,共32位比特位,所以它和 uint32 本质上也没有区别。它表示的是一个 Unicode字符(Unicode是一个可以表示世界范围内的绝大部分字符的编码规范)。

import (
    "fmt"
    "unsafe"
)

func main() {
    var a byte = ‘A‘
    var b rune = ‘B‘
    fmt.Printf("a 占用 %d 个字节数\nb 占用 %d 个字节数", unsafe.Sizeof(a), unsafe.Sizeof(b))
}

输出如下

a 占用 1 个字节数
b 占用 4 个字节数

由于 byte 类型能表示的值是有限,只有 2^8=256 个。所以如果你想表示中文的话,你只能使用 rune 类型。

var name rune = ‘中‘

byte 和 uint8 没有区别,rune 和 uint32 没有区别,那为什么还要多出一个 byte 和 rune 类型呢?

理由很简单,因为uint8 和 uint32 ,直观上让人以为这是一个数值,但是实际上,它也可以表示一个字符,所以为了消除这种直观错觉,就诞生了 byte 和 rune 这两个别名类型。

字符串

字符串,可以说是大家很熟悉的数据类型之一。定义方法很简单

var mystr string = "hello"

上面说的byte 和 rune 都是字符类型,若多个字符放在一起,就组成了字符串,也就是这里要说的 string 类型。

比如 hello ,对照 ascii 编码表,每个字母对应的编号是:104,101,108,108,111

import (
    "fmt"
)

func main() {
    var mystr01 string = "hello"
    var mystr02 [5]byte = [5]byte{104, 101, 108, 108, 111}
    fmt.Printf("mystr01: %s\n", mystr01)
    fmt.Printf("mystr02: %s", mystr02)
}

输出如下,mystr01 和 mystr02 输出一样,说明了 string 的本质,其实是一个 byte数组

mystr01: hello
mystr02: hello

通过以上学习,我们知道字符分为 byte 和 rune,占用的大小不同。

这里来考一下大家,hello,中国 占用几个字节?

要回答这个问题,你得知道 Go 语言的 string 是用 uft-8 进行编码的,英文字母占用一个字节,而中文字母占用 3个字节,所以 hello,中国 的长度为 5+1+(3*2)= 12个字节。

import (
    "fmt"
)

func main() {
    var country string = "hello,中国"
    fmt.Println(len(country))
}
// 输出
12

以上虽然我都用双引号表示 一个字符串,但这并不是字符串的唯一表示方式。

除了双引号之外 ,你还可以使用反引号。

大多情况下,二者并没有区别,但如果你的字符串中有转义字符\ ,这里就要注意了,它们是有区别的。

使用反引号包裹的字符串,相当于 Python 中的 raw 字符串,会忽略里面的转义。

比如我想表示 \r\n 这个 字符串,使用双引号是这样写的,这种叫解释型表示法

var mystr01 string = "\\r\\n"

而使用反引号,就方便多了,所见即所得,这种叫原生型表示法

var mystr02 string = `\r\n`

他们的打印结果 都是一样的

import (
    "fmt"
)

func main() {
    var mystr01 string = "\\r\\n"
    var mystr02 string = `\r\n`
    fmt.Println(mystr01)
    fmt.Println(mystr02)
}

// output
\r\n
\r\n

如果你仍然想使用解释型的字符串,但是各种转义实在太麻烦了。你可以使用 fmt 的 %q 来还原一下。

import (
    "fmt"
)

func main() {
    var mystr01 string = `\r\n`
    fmt.Print(`\r\n`)
    fmt.Printf("的解释型字符串是: %q", mystr01)
}

输出如下

\r\n的解释型字符串是: "\\r\\n"

同时反引号可以不写换行符(因为没法写)来表示一个多行的字符串。

import (
    "fmt"
)

func main() {
    var mystr01 string = `你好呀!
gogogo`

    fmt.Println(mystr01)
}

输出如下

你好呀!
gogogo

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。

声明数组,并给该数组里的每个元素赋值(索引值的最小有效值和其他大多数语言一样是 0,不是1)

// [3] 里的3 表示该数组的元素个数及容量
var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3

声明并直接初始化数组

// 第一种方法
var arr [3]int = [3]int{1,2,3}

// 第二种方法
arr := [3]int{1,2,3}

上面的 3 表示数组的元素个数 ,万一你哪天想往该数组中增加元素,你得对应修改这个数字,为了避免这种硬编码,你可以这样写,使用 ... 让Go语言自己根据实际情况来分配空间。

arr := [...]int{1,2,3}

[3]int[4]int 虽然都是数组,但他们却是不同的类型,使用 fmt 的 %T 可以查得。

import (
    "fmt"
)

func main() {
    arr01 := [...]int{1, 2, 3}
    arr02 := [...]int{1, 2, 3, 4}
    fmt.Printf("%d 的类型是: %T\n", arr01, arr01)
    fmt.Printf("%d 的类型是: %T", arr02, arr02)
}

输出 如下

[1 2 3] 的类型是: [3]int
[1 2 3 4] 的类型是: [4]int

如果你觉得每次写 [3]int 有点麻烦,你可以为 [3]int 定义一个类型字面量,也就是别名类型。

使用 type 关键字可以定义一个类型字面量,后面只要你想定义一个容器大小为3,元素类型为int的数组 ,都可以使用这个别名类型。

import (
    "fmt"
)

func main() {
    type arr3 [3]int

    myarr := arr3{1,2,3}
    fmt.Printf("%d 的类型是: %T", myarr, myarr)
}

输出 如下

[1 2 3] 的类型是: main.arr3

其实定义数组还有一种偷懒的方法,比如下面这行代码

arr:=[4]int{2:3}

打印 arr,会是

[0 0 3 0]

可以看出[4]int{2:3},4表示数组有4个元素,2 和 3 分别表示该数组索引为2(初始索引为0)的值为3,而其他没有指定值的,就是 int 类型的零值,即0。

切片

切片(Slice)与数组一样,也是可以容纳若干类型相同的元素的容器。与数组不同的是,无法通过切片类型来确定其值的长度。每个切片值都会将数组作为其底层数据结构。我们也把这样的数组称为切片的底层数组。

切片是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内(意思是这是个左闭右开的区间)

import (
    "fmt"
)

func main() {
    myarr := [...]int{1, 2, 3}
    fmt.Printf("%d 的类型是: %T", myarr[0:2], myarr[0:2])
}

输出 如下

[1 2] 的类型是: []int

切片的构造,有四种方式

  1. 对数组进行片段截取,主要有如下两种写法

    // 定义一个数组
    myarr := [5]int{1,2,3,4,5}
    
    // 【第一种】
    // 1 表示从索引1开始,直到到索引为 2 (3-1)的元素
    mysli1 := myarr[1:3]
    
    // 【第二种】
    // 1 表示从索引1开始,直到到索引为 2 (3-1)的元素
    mysli2 := myarr[1:3:4]
    

    如果你把上面的 mysli1mysli2 打印出来,会发现他们居然是一样的。那第二种的 myarr[1:3:4] 的 4有什么用呢?

    //注意: 第三个4可不是步长, 而是用来指定容量的, 但它又不完全等于容量 mysli2 := myarr[1:3:4] //arr[start?:end:?cap], 此时 cap - start 才是容量, 所以这里的容量是4-1=3 //所以这里的cap无论何时都不能超过底层数组的元素个数, 并且不能小于结束位置

    用下面这段代码来验证一下

    package main
    
    import "fmt"
    
    func main(){
        myarr := [5]int{1,2,3,4,5}
        fmt.Printf("myarr 的长度为:%d,容量为:%d\n", len(myarr), cap(myarr))
    
        mysli1 := myarr[1:3]
        fmt.Printf("mysli1 的长度为:%d,容量为:%d\n", len(mysli1), cap(mysli1))
        fmt.Println(mysli1)
    
        mysli2 := myarr[1:3:4]
        fmt.Printf("mysli2 的长度为:%d,容量为:%d\n", len(mysli2), cap(mysli2))
        fmt.Println(mysli2)
    }
    

    输出如下,说明切片的第三个数,影响的只是切片的容量,而不会影响长度

    myarr 的长度为:5,容量为:5
    mysli1 的长度为:2,容量为:4
    [2 3]
    mysli2 的长度为:2,容量为:3
    [2 3]
    
  2. 从头声明赋值(例子如下)

    // 声明字符串切片
    var strList []string
    
    // 声明整型切片
    var numList []int
    
    // 声明一个空切片
    var numListEmpty = []int{}
    
  3. 使用 make 函数构造,make 函数的格式:make( []Type, size, cap )

    这个函数刚好指出了,一个切片具备的三个要素:类型(Type),长度(size),容量(cap)

    import (
     "fmt"
    )
    
    func main() {
     a := make([]int, 2)
     b := make([]int, 2, 10)
     fmt.Println(a, b)
     fmt.Println(len(a), len(b))
     fmt.Println(cap(a), cap(b))
    }
    

    输出 如下

    [0 0] [0 0]
    2 2
    2 10
    
  4. 使用和数组一样,偷懒的方法

    import (
     "fmt"
    )
    
    func main() {
        a := []int{4:2}
        fmt.Println(a)
        fmt.Println(len(a), cap(a))
    }
    

    输出如下

    [0 0 0 0 2]
    5 5
    

关于 len 和 cap 的概念,可能不好理解 ,这里举个例子:

  • 公司名,相当于字面量,也就是变量名。
  • 公司里的所有工位,相当于已分配到的内存空间
  • 公司里的员工,相当于元素。
  • cap 代表你这个公司最多可以容纳多少员工
  • len 代表你这个公司当前有多少个员工

由于 切片是引用类型,所以你不对它进行赋值的话,它的零值(默认值)是 nil

var myarr []int
fmt.Println(myarr == nil)
// true

数组 与 切片 有相同点,它们都是可以容纳若干类型相同的元素的容器

也有不同点,数组的容器大小固定,而切片本身是引用类型,它更像是 Python 中的 list ,我们可以对它 append 进行元素的添加。

import (
    "fmt"
)

func main() {
    myarr := []int{1}
    // 追加一个元素
    myarr = append(myarr, 2)
    // 追加多个元素
    myarr = append(myarr, 3, 4)
    // 追加一个切片, ... 表示解包,不能省略
    myarr = append(myarr, []int{7, 8}...)
    // 在第一个位置插入元素
    myarr = append([]int{0}, myarr...)
    // 在中间插入一个切片(两个元素)
    myarr = append(myarr[:5], append([]int{5,6}, myarr[5:]...)...)
    fmt.Println(myarr)
}

输出 如下

[0 1 2 3 4 5 6 7 8]

每一次append之后,容器大小会扩大为原来的两倍,但字节大小超过1024时,每次扩展上一次的1/4

Map

字典(Map 类型),是由若干个 key:value 这样的键值对映射组合在一起的数据结构。

它是哈希表的一个实现,这就要求它的每个映射里的key,都是唯一的,可以使用 ==!= 来进行判等操作,换句话说就是key必须是可哈希的。

什么叫可哈希的?简单来说,一个不可变对象,都可以用一个哈希值来唯一表示,这样的不可变对象,比如字符串类型的对象(可以说除了切片、 字典,函数之外的其他内建类型都算)。

意思就是,你的 key 不能是切片,不能是字典,不能是函数。。

字典由key和value组成,它们各自有各自的类型。

在声明字典时,必须指定好你的key和value是什么类型的,然后使用 map 关键字来告诉Go这是一个字典。

map[KEY_TYPE]VALUE_TYPE

声明初始化字典

三种声明并初始化字典的方法

// 第一种方法
var scores map[string]int = map[string]int{"english": 80, "chinese": 85}

// 第二种方法
scores := map[string]int{"english": 80, "chinese": 85}

// 第三种方法
scores := make(map[string]int)
scores["english"] = 80
scores["chinese"] = 85

要注意的是,第一种方法如果拆分成多步(声明、初始化、再赋值),和其他两种有很大的不一样了,相对会比较麻烦。

import "fmt"

func main() {
    // 声明一个名为 score 的字典
    var scores map[string]int

    // 未初始化的 score 的零值为nil,无法直接进行赋值
    if scores == nil {
        // 需要使用 make 函数先对其初始化
        scores = make(map[string]int)
    }

    // 经过初始化后,就可以直接赋值
    scores["chinese"] = 90
    fmt.Println(scores)
}

字典的相关操作

添加元素

scores["math"] = 95

更新元素,若key已存在,则直接更新value

scores["math"] = 100

读取元素,直接使用 [key] 即可 ,如果 key 不存在,也不报错,会返回其value-type 的零值。

fmt.Println(scores["math"])

删除元素,使用 delete 函数,如果 key 不存在,delete 函数会静默处理,不会报错。

delete(scores, "math")

当访问一个不存在的key时,并不会直接报错,而是会返回这个 value 的零值,如果 value的类型是int,就返回0。

package main

import "fmt"

func main() {
    scores := make(map[string]int)
    fmt.Println(scores["english"]) // 输出 0
}

判断 key 是否存在

当key不存在,会返回value-type的零值 ,所以你不能通过返回的结果是否是零值来判断对应的 key 是否存在,因为 key 对应的 value 值可能恰好就是零值。

其实字典的下标读取可以返回两个值,使用第二个返回值都表示对应的 key 是否存在,若存在ok为true,若不存在,则ok为false

import "fmt"

func main() {
    scores := map[string]int{"english": 80, "chinese": 85}
    math, ok := scores["math"]
    if ok {
        fmt.Printf("math 的值是: %d", math)
    } else {
        fmt.Println("math 不存在")
    }
}

我们将上面的代码再优化一下

import "fmt"

func main() {
    scores := map[string]int{"english": 80, "chinese": 85}
    if math, ok := scores["math"]; ok {
        fmt.Printf("math 的值是: %d", math)
    } else {
        fmt.Println("math 不存在")
    }
}

如何对字典进行循环

Go 语言中没有提供类似 Python 的 keys() 和 values() 这样方便的函数,想要获取,你得自己循环。

循环还分三种

  1. 获取 key 和 value
import "fmt"

func main() {
    scores := map[string]int{"english": 80, "chinese": 85}

    for subject, score := range scores {
        fmt.Printf("key: %s, value: %d\n", subject, score)
    }
}
  1. 只获取key,这里注意不用占用符。
import "fmt"

func main() {
    scores := map[string]int{"english": 80, "chinese": 85}

    for subject := range scores {
        fmt.Printf("key: %s\n", subject)
    }
}
  1. 只获取 value,用一个占位符替代。
import "fmt"

func main() {
    scores := map[string]int{"english": 80, "chinese": 85}

    for _, score := range scores {
        fmt.Printf("value: %d\n", score)
    }
}

指针

什么是指针

当我们定义一个变量 name

var name string = "Go"

此时,name 是变量名,它只是编程语言中方便程序员编写和理解代码的一个标签。

当我们访问这个标签时,机算机会返回给我们它指向的内存地址里存储的值:`Go。

出于某些需要,我们会将这个内存地址赋值给另一个变量名,通常叫做 ptr(pointer的简写),而这个变量,我们称之为指针变量。

换句话说,指针变量(一个标签)的值是指针,也就是内存地址。

根据变量指向的值,是否是内存地址,我把变量分为两种:

  • 普通变量:存数据值本身
  • 指针变量:存值的内存地址

指针的创建

指针创建有三种方法

第一种方法

先定义对应的变量,再通过变量取得内存地址,创建指针

// 定义普通变量
aint := 1
// 定义指针变量
ptr := &aint

第二种方法

先创建指针,分配好内存后,再给指针指向的内存地址写入对应的值。

// 创建指针
astr := new(string)
// 给指针赋值
*astr = "Go"

第三种方法

先声明一个指针变量,再从其他变量取得内存地址赋值给它

aint := 1
var bint *int  // 声明一个指针
bint = &aint   // 初始化

上面的三段代码中,指针的操作都离不开这两个符号:

  • & :从一个普通变量中取得内存地址
  • *:当*在赋值操作符(=)的右边,是从一个指针变量中取得变量值,当*在赋值操作符(=)的左边,是指该指针指向的变量

通过下面这段代码,你可以熟悉这两个符号的用法

package main

import "fmt"

func main() {
    aint := 1     // 定义普通变量
    ptr := &aint  // 定义指针变量
    fmt.Println("普通变量存储的是:", aint)
    fmt.Println("普通变量存储的是:", *ptr)
    fmt.Println("指针变量存储的是:", &aint)
    fmt.Println("指针变量存储的是:", ptr)
}

输出如下

普通变量存储的是: 1
普通变量存储的是: 1
指针变量存储的是: 0xc0000100a0
指针变量存储的是: 0xc0000100a0

要想打印指针指向的内存地址,方法有两种

// 第一种
fmt.Printf("%p", ptr)

// 第二种
fmt.Println(ptr)

指针的类型

我们知道字符串的类型是 string,整型是int,那么指针如何表示呢?

写段代码试验一下就知道了

package main

import "fmt"

func main() {
    astr := "hello"
    aint := 1
    abool := false
    arune := ‘a‘
    afloat := 1.2

    fmt.Printf("astr 指针类型是:%T\n", &astr)
    fmt.Printf("aint 指针类型是:%T\n", &aint)
    fmt.Printf("abool 指针类型是:%T\n", &abool)
    fmt.Printf("arune 指针类型是:%T\n", &arune)
    fmt.Printf("afloat 指针类型是:%T\n", &afloat)
}

输出如下,可以发现用 *+所指向变量值的数据类型,就是对应的指针类型。

astr 指针类型是:*string
aint 指针类型是:*int
abool 指针类型是:*bool
arune 指针类型是:*int32
afloat 指针类型是:*float64

所以若我们定义一个只接收指针类型的参数的函数,可以这么写

func mytest(ptr *int)  {
    fmt.Println(*ptr)
}

指针的零值

当指针声明后,没有进行初始化,其零值是 nil。

func main() {
    a := 25
    var b *int  // 声明一个指针

    if b == nil {
        fmt.Println(b)
        b = &a  // 初始化:将a的内存地址给b
        fmt.Println(b)
    }
}

输出如下

<nil>
0xc0000100a0

指针与切片

切片与指针一样,都是引用类型。

如果我们想通过一个函数改变一个数组的值,有两种方法

  1. 将这个数组的切片做为参数传给函数
  2. 将这个数组的指针做为参数传给函数

尽管二者都可以实现我们的目的,但是按照 Go 语言的使用习惯,建议使用第一种方法,因为第一种方法,写出来的代码会更加简洁,易读。具体你可以参数下面两种方法的代码实现

使用切片

func modify(sls []int) {
    sls[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(a[:])
    fmt.Println(a)
}

使用指针

func modify(arr *[3]int) {
    (*arr)[0] = 90
}

func main() {
    a := [3]int{89, 90, 91}
    modify(&a)
    fmt.Println(a)
}

结构体

在之前学过的数据类型中,数组与切片,只能存储同一类型的变量。若要存储多个类型的变量,就需要用到结构体,它是将多个任意类型的变量组合在一起的聚合数据类型。

每个变量都成为该结构体的成员变量。

可以理解为 Go语言 的结构体struct和其他语言的class有相等的地位,但是Go语言放弃大量面向对象的特性,所有的Go语言类型除了指针类型外,都可以有自己的方法,提高了可扩展性。

在 Go 语言中没有没有 class 类的概念,只有 struct 结构体的概念,因此也没有继承,本篇文章,带你学习一下结构体相关的内容。

定义结构体

声明结构体

type 结构体名 struct {
    属性名   属性类型
    属性名   属性类型
    ...
}

比如我要定义一个可以存储个人资料名为 Profile 的结构体,可以这么写

type Profile struct {
    name   string
    age    int
    gender string
    mother *Profile // 指针
    father *Profile // 指针
}

匿名结构体

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

package main

import (
    "fmt"
)

func main() {
    var user struct{Name string; Age int}
    user.Name = "pprof.cn"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

创建指针类型结构体

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

    var p2 = new(person)
    fmt.Printf("%T\n", p2)     //*main.person
    fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

var p2 = new(person)
p2.name = "测试"
p2.age = 18
p2.city = "北京"
fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}

使用键值对初始化

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

p5 := person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}

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

p6 := &person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}

接口

见面向对象


流程控制

  • if - else 条件语句
  • switch - case 选择语句
  • for - range 循环语句
  • defer 延迟执行

If

条件语句模型

Go 里的条件语句模型是这样的

if 条件 1 {
  分支 1
} else if 条件 2 {
  分支 2
} else if 条件 ... {
  分支 ...
} else {
  分支 else
}

Go编译器,对于 {} 的位置有严格的要求,它要求 else if (或 else)和 两边的花括号,必须在同一行。

由于 Go是 强类型,所以要求你条件表达式必须严格返回布尔型的数据(nil 和 0 和 1 都不行)。

对于这个模型,分别举几个例子来看一下。

单分支判断

只有一个 if ,没有 else

import "fmt"

func main() {
    age := 20
    if age > 18 {
        fmt.Println("已经成年了")
    }
}

如果条件里需要满足多个条件,可以使用 &&||

  • &&:表示且,左右都需要为true,最终结果才能为 true,否则为 false
  • ||:表示或,左右只要有一个为true,最终结果即为true,否则 为 false
import "fmt"

func main() {
    age := 20
    gender := "male"
    if (age > 18 && gender == "male") {
        fmt.Println("是成年男性")
    }
}

多分支判断

if - else

import "fmt"

func main() {
    age := 20
    if age > 18 {
        fmt.Println("已经成年了")
    } else {
        fmt.Println("还未成年")
    }
}

if - else if - else

import "fmt"

func main() {
    age := 20
    if age > 18 {
        fmt.Println("已经成年了")
    } else if age >12 {
        fmt.Println("已经是青少年了")
    } else {
        fmt.Println("还不是青少年")
    }
}

高级写法

在 if 里可以允许先运行一个表达式,取得变量后,再对其进行判断,比如第一个例子里代码也可以写成这样

import "fmt"

func main() {
    if age := 20;age > 18 {
        fmt.Println("已经成年了")
    }
}

Switch

Go里的流程控制方法还是挺丰富,整理了下有如下这么多种:

  • if - else 条件语句
  • switch - case 选择语句
  • for - range 循环语句
  • goto 无条件跳转语句
  • defer 延迟执行

上一篇讲了 if -else 条件语句,今天先来讲讲 switch - case 选择语句。

语句模型

Go 里的选择语句模型是这样的

switch 表达式 {
    case 表达式1:
        代码块
    case 表达式2:
        代码块
    case 表达式3:
        代码块
    case 表达式4:
        代码块
    case 表达式5:
        代码块
    default:
        代码块
}

拿 switch 后的表达式分别和 case 后的表达式进行对比,只要有一个 case 满足条件,就会执行对应的代码块,然后直接退出 switch - case ,如果 一个都没有满足,才会执行 default 的代码块。

最简单的示例

switch 后接一个你要判断变量 education (学历),然后 case 会拿这个 变量去和它后面的表达式(可能是常量、变量、表达式等)进行判等。

如果相等,就执行相应的代码块。如果不相等,就接着下一个 case。

import "fmt"

func main() {
    education := "本科"

    switch education {
    case "博士":
        fmt.Println("我是博士")
    case "研究生":
        fmt.Println("我是研究生")
    case "本科":
        fmt.Println("我是本科生")
    case "大专":
        fmt.Println("我是大专生")
    case "高中":
        fmt.Println("我是高中生")
    default:
        fmt.Println("学历未达标..")
    }
}

输出如下

我是本科生

一个 case 多个条件

case 后可以接多个多个条件,多个条件之间是 的关系,用逗号相隔。

import "fmt"

func main() {
    month := 2

    switch month {
    case 3, 4, 5:
        fmt.Println("春天")
    case 6, 7, 8:
        fmt.Println("夏天")
    case 9, 10, 11:
        fmt.Println("秋天")
    case 12, 1, 2:
        fmt.Println("冬天")
    default:
        fmt.Println("输入有误...")
    }
}

输出如下

冬天

case 条件常量不能重复

当 case 后接的是常量时,该常量只能出现一次。

以下两种情况,在编译时,都会报错: duplicate case “male” in switch

错误案例一

gender := "male"

switch gender {
    case "male":
        fmt.Println("男性")
    // 与上面重复
    case "male":
        fmt.Println("男性")
    case "female":
        fmt.Println("女性")
}

错误案例二

gender := "male"

switch gender {
    case "male", "male":
        fmt.Println("男性")
    case "female":
        fmt.Println("女性")
}

switch 后可接函数

switch 后面可以接一个函数,只要保证 case 后的值类型与函数的返回值 一致即可。

import "fmt"

// 判断一个同学是否有挂科记录的函数
// 返回值是布尔类型
func getResult(args ...int) bool {
    for _, i := range args {
        if i < 60 {
            return false
        }
    }
    return true
}

func main() {
    chinese := 80
    english := 50
    math := 100

    switch getResult(chinese, english, math) {
    // case 后也必须 是布尔类型
    case true:
        fmt.Println("该同学所有成绩都合格")
    case false:
        fmt.Println("该同学有挂科记录")
    }
}

switch 可不接表达式

switch 后可以不接任何变量、表达式、函数。

当不接任何东西时,switch - case 就相当于 if - elseif - else

score := 30

switch {
    case score >= 95 && score <= 100:
        fmt.Println("优秀")
    case score >= 80:
        fmt.Println("良好")
    case score >= 60:
        fmt.Println("合格")
    case score >= 0:
        fmt.Println("不合格")
    default:
        fmt.Println("输入有误...")
}

switch 的穿透能力

正常情况下 switch - case 的执行顺序是:只要有一个 case 满足条件,就会直接退出 switch - case ,如果 一个都没有满足,才会执行 default 的代码块。

但是有一种情况是例外。

那就是当 case 使用关键字 fallthrough 开启穿透能力的时候。

s := "hello"
switch {
case s == "hello":
    fmt.Println("hello")
    fallthrough
case s != "world":
    fmt.Println("world")
}

代码输出如下:

hello
world

需要注意的是,fallthrough 只能穿透一层,意思是它让你直接执行下一个case的语句,而且不需要判断条件。

s := "hello"
switch {
case s == "hello":
    fmt.Println("hello")
    fallthrough
case s == "xxxx":
    fmt.Println("xxxx")
case s != "world":
    fmt.Println("world")
}

输出如下,并不会输出 world(即使它符合条件)

hello
xxxx

For

语句模型

这是 for 循环的基本模型。

for [condition |  ( init; condition; increment ) | Range]
{
   statement(s);
}

可以看到 for 后面,可以接三种类型的表达式。

  1. 接一个条件表达式
  2. 接三个表达式
  3. 接一个 range 表达式

但其实还有第四种

  1. 不接表达式

接一个条件表达式

这个例子会打印 1 到 5 的数值。

a := 1
for a <= 5 {
    fmt.Println(a)
    a ++
}

输出如下

1
2
3
4
5

接三个表达式

for 后面,紧接着三个表达式,使用 ; 分隔。

这三个表达式,各有各的用途

  • 第一个表达式:初始化控制变量,在整个循环生命周期内,只运行一次;
  • 第二个表达式:设置循环控制条件,当返回true,继续循环,返回false,结束循环;
  • 第三个表达式:每次循完开始(除第一次)时,给控制变量增量或减量。

这边的例子和上面的例子,是等价的。

import "fmt"

func main() {
    for i := 1; i <= 5; i++ {
        fmt.Println(i)
    }
}

输出如下

1
2
3
4
5

不接表达式:无限循环

在 Go 语言中,没有 while 循环,如果要实现无限循环,也完全可以 for 来实现。

当你不加任何的判断条件时, 就相当于你每次的判断都为 true,程序就会一直处于运行状态,但是一般我们并不会让程序处于死循环,在满足一定的条件下,可以使用关键字 break 退出循环体,也可以使用 continue 直接跳到下一循环。

下面两种写法都是无限循环的写法。

for {
    代码块
}

// 等价于
for ;; {
    代码块
}

举个例子

import "fmt"

func main() {
    var i int = 1
    for {
        if i > 5 {
            break
        }
        fmt.Printf("hello, %d\n", i)
        i++
    }
}

输出如下

hello, 1
hello, 2
hello, 3
hello, 4
hello, 5

接 for-range 语句

遍历一个可迭代对象,是一个很常用的操作。在 Go 可以使用 for-range 的方式来实现。

range 后可接数组、切片,字符串等

由于 range 会返回两个值:索引和数据,若你后面的代码用不到索引,需要使用 _ 表示 。

import "fmt"

func main() {
    myarr := [...]string{"world", "python", "go"}
    for _, item := range myarr {
        fmt.Printf("hello, %s\n", item)
    }
}

输出如下

hello, world
hello, python
hello, go

如果你用一个变量来接收的话,接收到的是索引

import "fmt"

func main() {
    myarr := [...]string{"world", "python", "go"}
    for i := range myarr {
        fmt.Printf("hello, %v\n", i)
    }
}

输出如下

hello, 0
hello, 1
hello, 2

Defer

延迟调用

defer 的用法很简单,只要在后面跟一个函数的调用,就能实现将这个 xxx 函数的调用延迟到当前函数执行完后再执行。

defer xxx()

这是一个很简单的例子,可以很快帮助你理解 defer 的使用效果。

import "fmt"

func myfunc() {
    fmt.Println("B")
}

func main() {
    defer myfunc()
    fmt.Println("A")
}

输出如下

A
B

当然了,对于上面这个例子可以简写为成如下,输出结果是一致的

import "fmt"

func main() {
    defer fmt.Println("B")
    fmt.Println("A")
}

即时求值的变量快照

使用 defer 只是延时调用函数,此时传递给函数里的变量,不应该受到后续程序的影响。

比如这边的例子

import "fmt"

func main() {
    name := "go"
    defer fmt.Println(name) // 输出: go

    name = "python"
    fmt.Println(name)      // 输出: python
}

输出如下,可见给 name 重新赋值为 python,后续调用 defer 的时候,仍然使用未重新赋值的变量值,就好在 defer 这里,给所有的这是做了一个快照一样。

python
go

如果 defer 后面跟的是匿名函数,情况会有所不同, defer 会取到最后的变量值

package main

import "fmt"


func main() {
    name := "go"
    defer func(){
    fmt.Println(name) // 输出: python
}()
    name = "python"
    fmt.Println(name)      // 输出: python
}

多个defer 反序调用

当我们在一个函数里使用了 多个defer,那么这些defer 的执行函数是如何的呢?

做个试验就知道了

import "fmt"

func main() {
    name := "go"
    defer fmt.Println(name) // 输出: go

    name = "python"
    defer fmt.Println(name) // 输出: python

    name = "java"
    fmt.Println(name)
}

输出如下,可见 多个defer 是反序调用的,有点类似栈一样,后进先出。

java
python
go

defer 与 return 孰先孰后

至此,defer 还算是挺好理解的。在一般的使用上,是没有问题了。

在这里提一个稍微复杂一点的问题,defer 和 return 到底是哪个先调用?

使用下面这段代码,可以很容易的观察出来

import "fmt"

var name string = "go"

func myfunc() string {
    defer func() {
        name = "python"
    }()

    fmt.Printf("myfunc 函数里的name:%s\n", name)
    return name
}

func main() {
    myname := myfunc()
    fmt.Printf("main 函数里的name: %s\n", name)
    fmt.Println("main 函数里的myname: ", myname)
}

输出如下

myfunc 函数里的name:go
main 函数里的name: python
main 函数里的myname:  go

来一起理解一下这段代码,第一行很直观,name 此时还是全局变量,值还是go

第二行也不难理解,在 defer 里改变了这个全局变量,此时name的值已经变成了 python

重点在第三行,为什么输出的是 go ?

解释只有一个,那就是 defer 是return 后才调用的。所以在执行 defer 前,myname 已经被赋值成 go 了。


函数

函数是基于功能或 逻辑进行封装的可复用的代码结构。将一段功能复杂、很长的一段代码封装成多个代码片段(即函数),有助于提高代码可读性和可维护性。

在 Go 语言中,函数可以分为两种:

  • 带有名字的普通函数
  • 没有名字的匿名函数

由于 Go语言是编译型语言,所以函数编写的顺序是无关紧要的,它不像 Python 那样,函数在位置上需要定义在调用之前。

函数的声明

函数的声明,使用 func 关键字,后面依次接 函数名参数列表返回值列表用 {} 包裹的代码逻辑体

func 函数名(形式参数列表)(返回值列表){
    函数体
}
  • 形式参数列表描述了函数的参数名以及参数类型,这些参数作为局部变量,其值由函数调用者提供
  • 返回值列表描述了函数返回值的变量名以及类型,如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

函数实现可变参数

可变参数分为几种:

  • 多个类型一致的参数
  • 多个类型不一致的参数

多个类型一致的参数

首先是多个类型一致的参数。

这边定义一个可以对多个数值进行求和的函数,

使用 ...int,表示一个元素为int类型的切片,用来接收调用者传入的参数。

// 使用 ...类型,表示一个元素为int类型的切片
func sum(args ...int) int {
    var sum int
    for _, v := range args {
        sum += v
    }
    return sum
}
func main() {
    fmt.Println(sum(1, 2, 3))
}

// output: 6

其中 ... 是 Go 语言为了方便程序员写代码而实现的语法糖,如果该函数下有多个类型的参数,这个语法糖必须得是最后一个参数。

同时这个语法糖,只能在定义函数时使用。

多个类型不一致的参数

上面那个例子中,我们的参数类型都是 int,如果你希望传多个参数且这些参数的类型都不一样,可以指定类型为 ...interface{} ,然后再遍历。

比如下面这段代码,是Go语言标准库中 fmt.Printf() 的函数原型:

import "fmt"
func MyPrintf(args ...interface{}) {
    for _, arg := range args {
        switch arg.(type) {
            case int:
                fmt.Println(arg, "is an int value.")
            case string:
                fmt.Println(arg, "is a string value.")
            case int64:
                fmt.Println(arg, "is an int64 value.")
            default:
                fmt.Println(arg, "is an unknown type.")
        }
    }
}

func main() {
    var v1 int = 1
    var v2 int64 = 234
    var v3 string = "hello"
    var v4 float32 = 1.234
    MyPrintf(v1, v2, v3, v4)
}

多个可变参数函数传递参数

上面提到了可以使用 ... 来接收多个参数,除此之外,它还有一个用法,就是用来解序列,将函数的可变参数(一个切片)一个一个取出来,传递给另一个可变参数的函数,而不是传递可变参数变量本身。

package main

import (
    "fmt"
)

func test(s string, n ...int) string {
    var x int
    for _, i := range n {
        x += i
    }

    return fmt.Sprintf(s, x)
}

func main() {
    s := []int{1, 2, 3}
    res := test("sum: %d", s...)    // slice... 展开slice
    println(res)
}

匿名函数

所谓匿名函数,就是没有名字的函数,它只有函数逻辑体,而没有函数名。

定义的格式如下

func(参数列表)(返回参数列表){
    函数体
}

对于那到只使用一次的函数,是没必要拥有姓名的。这才有了匿名函数。

有了这个背景,决定了匿名函数只有拥有短暂的生命,一般都是定义后立即使用。

就像这样,定义后立马执行(这里只是举例,实际代码没有意义)。

func(data int) {
    fmt.Println("hello", data)
}(100)

亦或是做为回调函数使用

// 第二个参数为函数
func visit(list []int, f func(int)) {
    for _, v := range list {
        // 执行回调函数
        f(v)
    }
}
func main() {
    // 使用匿名函数直接做为参数
    visit([]int{1, 2, 3, 4}, func(v int) {
        fmt.Println(v)
    })
}

闭包

package main

import (
    "fmt"
)

func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}

func main() {
    c := a()
    c()
    c()
    c()

    a() //不会输出i
}

输出结果:

    1
    2
    3

面向对象

结构体与继承

匿名字段

Go 语言本身并不支持继承,但我们可以使用组合的方法,实现类似继承的效果

在 Go 语言中,把一个结构体嵌入到另一个结构体的方法,称之为组合。

现在这里有一个表示公司(company)的结构体,还有一个表示公司职员(staff)的结构体。

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
}

将 company 这个 结构体嵌入到 staff 中,做为 staff 的一个匿名字段,staff 就直接拥有了 company 的所有属性了

type staff struct {
    name string
    age int
    gender string
    position string
    company   // 匿名字段
}
package main

import "fmt"

type company struct {
    companyName string
    companyAddr string
}

type staff struct {
    name string
    age int
    gender string
    position string
    company
}

func main()  {
    myCom := company{
        companyName: "Tencent",
        companyAddr: "深圳市南山区",
    }
    staffInfo := staff{
        name:     "小明",
        age:      28,
        gender:   "男",
        position: "云计算开发工程师",
        company: myCom,
    }

    fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.companyName)
    fmt.Printf("%s 在 %s 工作\n", staffInfo.name, staffInfo.company.companyName)
}

输出结果如下,可见staffInfo.companyNamestaffInfo.company.companyName 的效果是一样的。

小明 在 Tencent 工作
小明 在 Tencent 工作

同名字段

若两个结构体中有相同的字段名如 id int,

type company struct {
    id int
}

type staff struct {
    company
    id int
}

则 staff. id 是staff自己的属性,而staff.company.id为父类的属性

匿名指针字段

若一个结构体中存的是另一个结构体的指针

type person struct {
   name string
}
type student struct {
   *person//默认是nil
   id int
}

给指针对应的结构体赋值前先要new一个空间

func main() {
   var stu student
   stu.person = new(person)
   stu.name = "x"

   fmt.Println(stu.name)//x
}

另一种方法:

func main() {
   var stu student
   var per = person{"x"}
   stu.person = &per
   fmt.Println(stu.name)
}

方法

定义

Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

? 只能为当前包内命名类型定义方法。
? 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
? 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
? 不支持方法重载,receiver 只是参数签名的组成部分。
? 可用实例 value 或 pointer 调用全部方法,编译器自动转换。

一个方法就是一个包含了接受者的函数,接受者可以是命名类型(type)或者结构体类型的一个值或者是一个指针。

所有给定类型的方法属于该类型的方法集。

 func (recevier type) methodName(参数列表)(返回值列表){}

    参数和返回值可以省略
package main

type Test struct{}

// 无参数、无返回值
func (t Test) method0() {

}

// 单参数、无返回值
func (t Test) method1(i int) {

}

// 多参数、无返回值
func (t Test) method2(x, y int) {

}

// 无参数、单返回值
func (t Test) method3() (i int) {
    return
}

// 多参数、多返回值
func (t Test) method4(x, y int) (z int, err error) {
    return
}

// 无参数、无返回值
func (t *Test) method5() {

}

// 单参数、无返回值
func (t *Test) method6(i int) {

}

// 多参数、无返回值
func (t *Test) method7(x, y int) {

}

// 无参数、单返回值
func (t *Test) method8() (i int) {
    return
}

// 多参数、多返回值
func (t *Test) method9(x, y int) (z int, err error) {
    return
}

func main() {}

结构体类型方法

package main

import (
    "fmt"
)

//结构体
type User struct {
    Name  string
    Email string
}

//方法
func (u User) Notify() {
    fmt.Printf("%v : %v \n", u.Name, u.Email)
}
func main() {
    // 值类型调用方法
    u1 := User{"golang", "golang@golang.com"}
    u1.Notify()
    // 指针类型调用方法
    u2 := User{"go", "go@go.com"}
    u3 := &u2
    u3.Notify()
}

//结果
golang : golang@golang.com 
    go : go@go.com

普通函数与方法的区别

  1. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。

  2. 对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。

package main

//普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)

import (
    "fmt"
)

//1.普通函数
//接收值类型参数的函数
func valueIntTest(a int) int {
    return a + 10
}

//接收指针类型参数的函数
func pointerIntTest(a *int) int {
    return *a + 10
}

func structTestValue() {
    a := 2
    fmt.Println("valueIntTest:", valueIntTest(a))
    //函数的参数为值类型,则不能直接将指针作为参数传递
    //fmt.Println("valueIntTest:", valueIntTest(&a))
    //compile error: cannot use &a (type *int) as type int in function argument

    b := 5
    fmt.Println("pointerIntTest:", pointerIntTest(&b))
    //同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
    //fmt.Println("pointerIntTest:", pointerIntTest(b))
    //compile error:cannot use b (type int) as type *int in function argument
}

//2.方法
type PersonD struct {
    id   int
    name string
}

//接收者为值类型
func (p PersonD) valueShowName() {
    fmt.Println(p.name)
}

//接收者为指针类型
func (p *PersonD) pointShowName() {
    fmt.Println(p.name)
}

func structTestFunc() {
    //值类型调用方法
    personValue := PersonD{101, "hello world"}
    personValue.valueShowName()
    personValue.pointShowName()

    //指针类型调用方法
    personPointer := &PersonD{102, "hello golang"}
    personPointer.valueShowName()
    personPointer.pointShowName()

    //与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
}

func main() {
    structTestValue()
    structTestFunc()
}

输出结果:

    valueIntTest: 12
    pointerIntTest: 15
    hello world
    hello world
    hello golang
    hello golang

内部方法与外部方法

在 Go 语言中,方法名的首字母大小写非常重要,它被来实现控制对方法的访问权限。

  • 当方法的首字母为大写时,这个方法对于所有包都是Public,其他包可以随意调用
  • 当方法的首字母为小写时,这个方法是Private,其他包是无法访问的。

方法继承

子结构体可以调用父类的方法,但父类不能调用子类的方法

package main

import "fmt"

type person struct {
   name string
}
type student struct {
   person
   id int
}
//方法
func (p person)printInfo()  {
   fmt.Print(p.name)
}
func main() {
   var stu student
   stu.person.name = "x"
   stu.printInfo()
}

//结果
x

方法重写

绑定不同对象的方法名可以重复

package main

import "fmt"

type person struct {
   name string
}
type student struct {
   person
   id int
}
func (p person)printInfo()  {
   fmt.Println(p.name)
}
func (s student)printInfo()  {
   fmt.Println(s.name)
}
func main() {
   var stu student
   stu.person.name = "x"
   stu.printInfo()
   stu.person.printInfo()

}

接口

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

定义

使用 type 关键字来定义接口。

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

定义格式

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

实现

一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。

我们来定义一个Sayer接口:

// Sayer 接口
type Sayer interface {
    say()
}

定义dog和cat两个结构体:

type dog struct {}

type cat struct {}

因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}

接口类型变量

那实现了接口有什么用呢?

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}

指针接收者实现接口

同样的代码我们再来测试一下使用指针接收者有什么区别:

func (d *dog) move() {
    fmt.Println("狗会动")
}
func main() {
    var x Mover
    var wangcai = dog{} // 旺财是dog类型
    x = wangcai         // x不可以接收dog类型
    var fugui = &dog{}  // 富贵是*dog类型
    x = fugui           // x可以接收*dog类型
}

此时实现Mover接口的是*dog类型,所以不能给x传入dog类型的wangcai,此时x只能存储*dog类型的值。

多态

一个接口下,在不同对象上的不同表现。这就是多态

如:

func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a      
    x.say()     // 喵喵喵
    x = b      
    x.say()     // 汪汪汪
}

接口嵌套

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

// Sayer 接口
type Sayer interface {
    say()
}

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

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

嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口:

type cat struct {
    name string
}

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

func (c cat) move() {
    fmt.Println("猫会动")
}

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

空接口

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

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

func main() {
    // 定义一个空接口x
    var x interface{}
    s := "pprof.cn"
    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 studentInfo = make(map[string]interface{})
    studentInfo["name"] = "李白"
    studentInfo["age"] = 18
    studentInfo["married"] = false
    fmt.Println(studentInfo)

类型断言

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:

    x.(T)

其中:

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

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

举个例子:

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

上面的示例中如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}

关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。

异常处理

errors

package main

import (
   "errors"
   "fmt"
)

func main() {
   fmt.Println(s())
}
func s() error {
   err := errors.New("错误")
   return err
}

panic

在 Golang 中,有不少常规错误,在编译阶段就能提前告警,比如语法错误或类型错误等,但是有些错误仅能在程序运行后才能发生,比如数组访问越界、空指针引用等,这些运行时错误会引起程序退出。

触发panic

手动触发宕机,是非常简单的一件事,只需要调用 panic 这个内置函数即可,就像这样子

package main

func main() {
    panic("crash")
}

实际开发过程中并不会直接调用panic()函数,但是当程序遇到致命错误时,系统会自动调用该函数来终止整个程序的运行

recover

内建函数 – recover,它可以让程序在发生宕机后起生回生。

但是 recover 的使用,有一个条件,就是它必须在 defer 函数中才能生效,其他作用域下,它是不工作的。

这是一个简单的例子

import "fmt"

func set_data(x int) {
    defer func() {
        // recover() 可以将捕获到的panic信息打印
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()

    // 故意制造数组越界,触发 panic
    var arr [10]int
    arr[x] = 88
}

func main() {
    set_data(20)

    // 如果能执行到这句,说明panic被捕获了
    // 后续的程序能继续运行
    fmt.Println("everything is ok")
}

运行后,输出如下

$ go run main.go
runtime error: index out of range [20] with length 10
everything is ok

通常来说,不应该对进入 panic 宕机的程序做任何处理,但有时,需要我们可以从宕机中恢复,至少我们可以在程序崩溃前,做一些操作,举个例子,当 web 服务器遇到不可预料的严重问题时,在崩溃前应该将所有的连接关闭,如果不做任何处理,会使得客户端一直处于等待状态,如果 web 服务器还在开发阶段,服务器甚至可以将异常信息反馈到客户端,帮助调试。

总结

Golang 异常的抛出与捕获,依赖两个内置函数:

  • panic:抛出异常,使程序崩溃
  • recover:捕获异常,恢复程序或做收尾工作

revocer 调用后,抛出的 panic 将会在此处终结,不会再外抛,但是 recover,并不能任意使用,它有强制要求,必须得在 defer 下才能发挥用途。

IO操作

输入输出的底层原理

  • 终端其实是一个文件,相关实例如下:
    • os.Stdin:标准输入的文件实例,类型为*File
    • os.Stdout:标准输出的文件实例,类型为*File
    • os.Stderr:标准错误输出的文件实例,类型为*File

以文件的方式操作终端:

package main

import "os"

func main() {
    var buf [16]byte
    os.Stdin.Read(buf[:])
    os.Stdin.WriteString(string(buf[:]))
}

文件操作相关API

  • func Create(name string) (file *File, err Error)
    
    • 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666
  • func NewFile(fd uintptr, name string) *File
    
    • 根据文件描述符创建相应的文件,返回一个文件对象
  • func Open(name string) (file *File, err Error)
    
    • 只读方式打开一个名称为name的文件
  • func OpenFile(name string, flag int, perm uint32) (file *File, err Error)
    
    • 打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限
  • func (file *File) Write(b []byte) (n int, err Error)
    
    • 写入byte类型的信息到文件
  • func (file *File) WriteAt(b []byte, off int64) (n int, err Error)
    
    • 在指定位置开始写入byte类型的信息
  • func (file *File) WriteString(s string) (ret int, err Error)
    
    • 写入string信息到文件
  • func (file *File) Read(b []byte) (n int, err Error)
    
    • 读取数据到b中
  • func (file *File) ReadAt(b []byte, off int64) (n int, err Error)
    
    • 从off开始读取数据到b中
  • func Remove(name string) Error
    
    • 删除文件名为name的文件

打开和关闭文件

os.Open()函数能够打开一个文件,返回一个*File和一个err。对得到的文件实例调用close()方法能够关闭文件。

package main

import (
    "fmt"
    "os"
)

func main() {
    // 只读方式打开当前目录下的main.go文件
    file, err := os.Open("./main.go")
    if err != nil {
        fmt.Println("open file failed!, err:", err)
        return
    }
    // 关闭文件
    file.Close()
}

写文件

package main

import (
    "fmt"
    "os"
)

func main() {
    // 新建文件
    file, err := os.Create("./xxx.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()
    for i := 0; i < 5; i++ {
        file.WriteString("ab\n")
        file.Write([]byte("cd\n"))
    }
}

读文件

文件读取可以用file.Read()和file.ReadAt(),读到文件末尾会返回io.EOF的错误

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开文件
    file, err := os.Open("./xxx.txt")
    if err != nil {
        fmt.Println("open file err :", err)
        return
    }
    defer file.Close()
    // 定义接收文件读取的字节数组
    var buf [128]byte
    var content []byte
    for {
        n, err := file.Read(buf[:])
        if err == io.EOF {
            // 读取结束
            break
        }
        if err != nil {
            fmt.Println("read file err ", err)
            return
        }
        content = append(content, buf[:n]...)
    }
    fmt.Println(string(content))
}

拷贝文件

package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    // 打开源文件
    srcFile, err := os.Open("./xxx.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    // 创建新文件
    dstFile, err2 := os.Create("./abc2.txt")
    if err2 != nil {
        fmt.Println(err2)
        return
    }
    // 缓冲读取
    buf := make([]byte, 1024)
    for {
        // 从源文件读数据
        n, err := srcFile.Read(buf)
        if err == io.EOF {
            fmt.Println("读取完毕")
            break
        }
        if err != nil {
            fmt.Println(err)
            break
        }
        //写出去
        dstFile.Write(buf[:n])
    }
    srcFile.Close()
    dstFile.Close()
}

bufio

  • bufio包实现了带缓冲区的读写,是对文件读写的封装
  • bufio缓冲写数据
模式 含义
os.O_WRONLY 只写
os.O_CREATE 创建文件
os.O_RDONLY 只读
os.O_RDWR 读写
os.O_TRUNC 清空
os.O_APPEND 追加
  • bufio读数据
package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

func wr() {
    // 参数2:打开模式,所有模式d都在上面
    // 参数3是权限控制
    // w写 r读 x执行   w  2   r  4   x  1
    file, err := os.OpenFile("./xxx.txt", os.O_CREATE|os.O_WRONLY, 0666)
    if err != nil {
        return
    }
    defer file.Close()
    // 获取writer对象
    writer := bufio.NewWriter(file)
    for i := 0; i < 10; i++ {
        writer.WriteString("hello\n")
    }
    // 刷新缓冲区,强制写出
    writer.Flush()
}

func re() {
    file, err := os.Open("./xxx.txt")
    if err != nil {
        return
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        line, _, err := reader.ReadLine()
        if err == io.EOF {
            break
        }
        if err != nil {
            return
        }
        fmt.Println(string(line))
    }

}

func main() {
    re()
}

ioutil工具包

  • 工具包写文件
  • 工具包读取文件
package main

import (
   "fmt"
   "io/ioutil"
)

func wr() {
   err := ioutil.WriteFile("./yyy.txt", []byte("www.5lmh.com"), 0666)
   if err != nil {
      fmt.Println(err)
      return
   }
}

func re() {
   content, err := ioutil.ReadFile("./yyy.txt")
   if err != nil {
      fmt.Println(err)
      return
   }
   fmt.Println(string(content))
}

func main() {
   re()
}

例子

实现一个cat命令

使用文件操作相关知识,模拟实现linux平台cat命令的功能。

package main

import (
    "bufio"
    "flag"
    "fmt"
    "io"
    "os"
)

// cat命令实现
func cat(r *bufio.Reader) {
    for {
        buf, err := r.ReadBytes(‘\n‘) //注意是字符
        if err == io.EOF {
            break
        }
        fmt.Fprintf(os.Stdout, "%s", buf)
    }
}

func main() {
    flag.Parse() // 解析命令行参数
    if flag.NArg() == 0 {
        // 如果没有参数默认从标准输入读取内容
        cat(bufio.NewReader(os.Stdin))
    }
    // 依次读取每个指定文件的内容并打印到终端
    for i := 0; i < flag.NArg(); i++ {
        f, err := os.Open(flag.Arg(i))
        if err != nil {
            fmt.Fprintf(os.Stdout, "reading from %s failed, err:%v\n", flag.Arg(i), err)
            continue
        }
        cat(bufio.NewReader(f))
    }
}

Golang基础

上一篇:2021百度之星初赛 A迷失(DP+flody+二进制优化)


下一篇:Vue基础语法