GO:1.2 命令源码文件

我们已经知道,环境变量 GOPATH 指向的是一个或多个工作区,每个工作区中都会有以代码包为基本组织形式的源码文件。

这里的源码文件又分为三种,即:命令源码文件、库源码文件和测试源码文件,它们都有着不同的用途和编写规则。
command1.png

1、命令源码文件的用途是什么,怎样编写它?

1、命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的。我们可以通过构建或安装,生成与其对应的可执行文件,后者一般会与该命令源码文件的直接父目录同名。

2、如果一个源码文件声明属于main包,并且包含一个无参数声明且无结果声明的main函数,那么它就是命令源码文件。 就像下面这段代码:

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, world!")
}

如果你把这段代码存成 demo1.go 文件,那么运行go run demo1.go命令后就会在屏幕(标准输出)中看到Hello, world!

注:
1、当需要模块化编程时,我们往往会将代码拆分到多个文件,甚至拆分到不同的代码包中。但无论怎样,对于一个独立的程序来说,命令源码文件永远只会也只能有一个。如果有与命令源码文件同包的源码文件,那么它们也应该声明属于main包。

2、通过构建或安装命令源码文件,生成的可执行文件就可以被视为“命令”,既然是命令,那么就应该具备接收参数的能力。

2、命令源码文件怎样接收参数

先看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"flag"//1步
"fmt"
)

var name string

func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")//2步
}
/*
1、函数flag.StringVar接受 4 个参数。

2、第 1 个参数是用于存储该命令参数值的地址,具体到这里就是在前面声明的变量name的地址了,由表达式&name表示。

3、第 2 个参数是为了指定该命令参数的名称,这里是name。

4、第 3 个参数是为了指定在未追加该命令参数时的默认值,这里是everyone。

5、第 4 个函数参数,即是该命令参数的简短说明了,这在打印命令说明时会用到。
*/

func main() {
flag.Parse()//函数flag.Parse用于真正解析命令参数,并把它们的值赋给相应的变量。
//3步
fmt.Printf("Hello, %s!\n", name)
}

/*
对该函数的调用必须在所有命令参数存储载体的声明(这里是对变量name的声明)和设置(这里是在[2]处对flag.StringVar函数的调用)之后,并且在读取任何命令参数值之前进行。

正因为如此,我们最好把flag.Parse()放在main函数的函数体的第一行。
*/

注:还有一个与flag.StringVar函数类似的函数,叫flag.String。这两个函数的区别是,后者会直接返回一个已经分配好的用于存储命令参数值的地址。如果使用它的话,我们就需要把“var name string”改为

1
var name = flag.String("name", "everyone", "The greeting object.")

3、怎样在运行命令源码文件的时候传入参数,又怎样查看参数的使用说明

我们把上述代码存成名为 demo2.go 的文件,那么运行如下命令就可以为参数name传值:

1
go run demo2.go -name="Robert"

运行后,打印到标准输出(stdout)的内容会是:

1
Hello, Robert!

另外,如果想查看该命令源码文件的参数说明,可以这样做:

1
$ go run demo2.go --help

其中的$表示我们是在命令提示符后运行go run命令的。运行后输出的内容会类似:

1
2
3
4
Usage of /var/folders/ts/7lg_tl_x2gd_k1lm5g_48c7w0000gn/T/go-build155438482/b001/exe/demo2:
-name string
The greeting object. (default "everyone")
exit status 2

以下是go run命令构建上述命令源码文件时临时生成的可执行文件的完整路径

1
/var/folders/ts/7lg_tl_x2gd_k1lm5g_48c7w0000gn/T/go-build155438482/b001/exe/demo2

如果我们先构建这个命令源码文件再运行生成的可执行文件,像这样:

1
2
$ go build demo2.go
$ ./demo2 --help

那么输出就会是

1
2
3
Usage of ./demo2:
-name string
The greeting object. (default "everyone")

4、怎样自定义命令源码文件的参数使用说明

1、最简单的一种方式就是对变量flag.Usage重新赋值。

flag.Usage的类型是func(),即一种无参数声明且无结果声明的函数类型。flag.Usage变量在声明时就已经被赋值了,所以我们才能够在运行命令go run demo2.go –help时看到正确的结果。注意,对flag.Usage的赋值必须在调用flag.Parse函数之前。现在,我们把 demo2.go 另存为 demo3.go,然后在main函数体的开始处加入如下代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"flag"//1步
"fmt"
)

var name string

func init() {
flag.StringVar(&name, "name", "everyone", "The greeting object.")//2步
}

func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
flag.PrintDefaults()
}
flag.Parse()//函数flag.Parse用于真正解析命令参数,并把它们的值赋给相应的变量。
//3步
fmt.Printf("Hello, %s!\n", name)
}

运行

1
$ go run demo3.go --help

后,就会看到

1
2
3
4
Usage of question:
-name string
The greeting object. (default "everyone")
exit status 2

2、现在再深入一层,我们在调用flag包中的一些函数(比如StringVar、Parse等等)的时候,实际上是在调用flag.CommandLine变量的对应方法。

flag.CommandLine相当于默认情况下的命令参数容器。所以,通过对flag.CommandLine重新赋值,我们可以更深层次地定制当前命令源码文件的参数使用说明。

现在我们把main函数体中的那条对flag.Usage变量的赋值语句注销掉,然后在init函数体的开始处添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"flag"//1步
"fmt"
)

var name string

func init() {
flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
flag.CommandLine.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage of %s:\n", "question")
flag.PrintDefaults()
}
flag.StringVar(&name, "name", "everyone", "The greeting object.")//2步
}

func main() {
flag.Parse()//函数flag.Parse用于真正解析命令参数,并把它们的值赋给相应的变量。
//3步
fmt.Printf("Hello, %s!\n", name)
}

再运行命令go run demo3.go –help后,其输出会与上一次的输出的一致。不过后面这种定制的方法更加灵活。比如,当我们把为flag.CommandLine赋值的那条语句改为

1
flag.CommandLine = flag.NewFlagSet("", flag.PanicOnError)

后,再运行go run demo3.go –help命令就会产生另一种输出效果。这是由于我们在这里传给flag.NewFlagSet函数的第二个参数值是flag.PanicOnError。

  • 1、flag.PanicOnError和flag.ExitOnError都是预定义在flag包中的常量。
  • 2、flag.ExitOnError的含义是,告诉命令参数容器,当命令后跟–help或者参数设置的不正确的时候,在打印命令参数使用说明后以状态码2结束当前程序。
  • 状态码2代表用户错误地使用了命令,而flag.PanicOnError与之的区别是在最后抛出“运行时恐慌(panic)”。
  • 上述两种情况都会在我们调用flag.Parse函数时被触发。

3、下面再进一步,我们索性不用全局的flag.CommandLine变量,转而自己创建一个私有的命令参数容器。我们在函数外再添加一个变量声明:

1
var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)

然后,我们把对flag.StringVar的调用替换为对cmdLine.StringVar调用,再把flag.Parse()替换为cmdLine.Parse(os.Args[1:])。

其中的os.Args[1:]指的就是我们给定的那些命令参数。这样做就完全脱离了flag.CommandLine。

这样做的好处依然是更灵活地定制命令参数容器。但更重要的是,你的定制完全不会影响到那个全局变量flag.CommandLine。

GO:工作区和GOPATH

1、Go基础知识导图

go1.jpg

2、工作区和GOPATH

1、Go 3个环境变量

  • GOROOT:Go语言安装根目录的路径,也就是GO语言的安装路径
  • GOPATH:若干工作区目录的路径,是我们自己定义的工作空间
  • GOBIN:Go程序生成的可执行文件的路径

2、GOPATH有什么意义

  1. 可以把GOPATH简单理解成Go语言的工作目录,它的值是一个目录的路径,也可以是多个目录的路径,每个目录都代表Go语言的一个工作区。

  2. 我们需要利用这些工作区去放置Go语言的源码文件,以及安装(install)后的归档文件(archive file,也就是以.a为扩展名的文件)和可执行文件。

  3. Go语言项目在其生命周期内的所有操作(编码、依赖管理、构建、测试、安装等)基本都是围绕GOPATH和工作区进行的。

go build 命令一些可选项的用途和用法

1、Go 语言源码的组织方式是怎样的?

1、Go语言的源码也是以代码包为基本组织单位的。在文件系统中,这些代码包与目录一一对应。目录有子目录,代码包也可以有子包。

2、一个代码包可以包含任意个以.go为扩展名的原码文件,这些源码文件都需要被声明属于同一个代码包。

3、代码包的名称一般会与源码文件所在目录同名。如果不同名,在构建、安装时会以代码包名称为准。

4、每个代码包都有导入路径。代码包的导入路径是其他代码在使用该包中的程序实体时,需要引入的路径。例如:

1
import "github.com/labstack/echo"

5、在工作区中,一个代码包的导入路径实际上就是从src子目录到该包的实际存储位置的相对路径。

6、所以说,Go语言源码的组织方式就是以环境变量GOPATH、工作区、src目录个代码包为主线的。一般情况下,Go语言的源码文件都需要存放在环境变量GOPATH包含的某个工作区(目录)中的src目录下的某个代码包(目录)中。

2、源码安装后的结果是什么?(只有在安装后,Go语言源码才能被我们或其他代码使用)

1、源码文件通常会被放在某个工作区的src子目录下

2、安装后如果产生了归档文件(以.a为扩展名的文件)会被放进该工作区的pkg子目录

3、如果产生了可执行文件,就可能会被放在该工作区的bin子目录下。

归档文件存放具体位置和规则:
1、源码文件会以代码包的形式组织起来,一个代码包其实就对应一个目录。安装某个代码包而产生的归档文件就是与这个代码包同名的。

2、放置它的相对目录就是该代码包的导入路径的直接父级。比如,一个已存在的代码包的导入路径是:

1
github.com/labstack/echo

那么执行命令

1
go insatll github.com/labstack/echo

生成的归档文件的相对目录就是github.com/labstack,文件名为echo.a。

3、上面这个代码包导入路径还有一层含义,即:该代码包的源码文件存在于github网站的labstack组的代码仓库echo中

4、归档文件的相对目录与pkg目录之间还有一级目录,称为平台相关目录。平台相关目录的名称是由“build”的目标操作系统、下划线和目标计算架构的代号组成的。比如,构建某个代码包的目标擦欧总系统时Linux,目标计算架构是64位,对应的平台相关目录就是linux_amd64。
因此,上述代码包的归档文件就会被放置在当前工作区的子目录
pkg/linux_amd64/github.com/labstack中。
gopath与工作区:
gopath1.png

5、某个工作区的src子目录下的源码文件在安装后一般会被放置到当前工作区的pkg子目录下对应的目录中,或者直接被放置到该工作区的bin子目录中。

3、理解构建和安装Go程序的过程

构建与安装的异同:
1、构建使用命令go build,安装使用命令go install。构建和安装代码包的时候都会执行编译、打包等操作,并且,这些操作生成的任何文件都会先被保存到某个临时的目录中。

2、构建源码文件

  • 如果构建的是库源码文件

那么操作后产生的结果只会存在于临时目录中,这里构建的主要意义在于检查和验证;

  • 如果构建的是命令源码文件

操作的结果文件会被搬运到源码文件所在的目录中。

3、安装过程会先执行构建、然后还会进行你链接操作,并且把结果文件搬运到指定目录。如:

  • 安装库源码文件,结果文件被搬运到他所在工作区的pkg目录下的某个子目录中;
  • 安装命令源码文件,结果文件被搬运到他所在工作区的bin目录中,或者环境变量GOBIN指向的目录中。

4、go build 命令一些可选项的用途和用法

1、在运行go build命令的时候,默认不会编译目标代码包所依赖的那些代码包。

2、如果被依赖的代码包的归档文件不存在,或者源码文件有了变化,那它还是会被编译。

3、如果要强制编译它们,可以在执行命令的时候加入标记-a。此时,不但目标代码包总是会被编译,它依赖的代码包也总会被编译,即使依赖的是标准库中的代码包也是如此。

5、另外,如果不但要编译依赖的代码包,还要安装它们的归档文件,那么可以加入标记-i。

6、那么我们怎么确定哪些代码包被编译了呢?有两种方法。

  • 运行go build命令时加入标记-x,这样可以看到go build命令具体都执行了哪些操作。另外也可以加入标记-n,这样可以只查看具体操作而不执行它们。
  • 运行go build命令时加入标记-v,这样可以看到go build命令编译的代码包的名称。它在与-a标记搭配使用时很有用。

7、下面再说一说与 Go 源码的安装联系很紧密的一个命令:go get。
命令go get会自动从一些主流公用代码仓库(比如 GitHub)下载目标代码包,并把它们安装到环境变量GOPATH包含的第 1 工作区的相应目录中。如果存在环境变量GOBIN,那么仅包含命令源码文件的代码包会被安装到GOBIN指向的那个目录。

最常用的几个标记有下面几种。

1
2
3
4
5
6
7
8
9
-u:下载并安装代码包,不论工作区中是否已存在它们。

-d:只下载代码包,不安装代码包。

-fix:在下载代码包后先运行一个用于根据当前 Go 语言版本修正代码的工具,然后再安装代码包。

-t:同时下载测试所需的代码包。

-insecure:允许通过非安全的网络协议下载和安装代码包。HTTP 就是这样的协议。

hexo博客迁移

1、安装hexo博客必要的软件

1、下载安装Git客户端
2、安装node js

2、Github添加新电脑生成的密钥

打开git bash输入如下命令:

1
ssh-keygen -t rsa -C "xxxxx@xxx.com"

邮箱为GitHub注册邮箱,输入命令后直接回车,生成密钥对。根据提示找到密钥对所在位置,将id_rsa.pub公钥内容复制粘贴到Github-settings-‘SSH and GPG keys’-‘SSH keys’中。

使用

1
ssh -T git@github.com   测试公钥是否添加成功

3、备份原文件

需要转移的文件有:
hexo1
由于配置文件和主题文件需要经常更改,采用github创建博客分支的方式进行备份。

1、创建分支
克隆github上上生成的静态文件到hexo文件夹中

1
git clone https://github.com/yourname/xxxx.github.io.git hexo

克隆后将除.git文件外其他所有文件删除。主要是为了得到版本管理文件夹.git(.git文件为隐藏文件,可直接将可见文件全部删除)。

2、将备份的原文件复制到此文件夹
若文件夹是从github克隆,则需要删除主题文件中的版本控制文件夹,以next主题为例:

1
$ rm -rf thems/next/.git*

创建名为hexo的分支

1
$ git checkout -b hexo

保存所有文件到暂存区

1
$ git add --all

提交变更

1
$ git commit -m "hexo-2"

提交变更时报错:
hexo2
根据提示配置。
推送分支到github,并用–set-upstream与origin创建关联,将hexo设置为默认分支

1
$ git push --set-upstream origin hexo

4、迁移

以后在其他电脑上写博客,直接将分支克隆下来。再使用npm install安装依赖。

1
2
$ git clone -b hexo https://github.com/yourname/xxx.github.io.git
$ npm install

5、发表文章

1、新建文章

1
hexo n "xxx"

2、注意:需要使用git push把源文件推到分支上

1
2
3
$ git add .
$ git commit -m "xxxx"
$ git push origin hexo

3、部署文章

1
hexo d -g

参考:
1、https://fl4g.cn/2018/08/03/Hexo%E5%8D%9A%E5%AE%A2%E8%BF%81%E7%A7%BB%E5%88%B0%E5%85%B6%E4%BB%96%E7%94%B5%E8%84%91/

2、https://blog.csdn.net/white_idiot/article/details/80685990

测试驱动开发TDD

测试驱动开发TDD(Test Driven Development)

1、什么是 TDD

1、TDD 有广义和狭义之分

1、常说的是狭义的TDD,也就是单元测试驱动开发UTDD(Unit Test Driven Development);

2、广义的TDD:是验收测试驱动开发ATDD(Acceptance Test Driven Development),
包括行为驱动开发BDD(Behavior Driven Development)和消费者驱动契约开发Consumer-Driven Contracts Development 等。

2、TDD 有三层含义:

  • Test-Driven Development,测试驱动开发。
  • Task-Driven Development,任务驱动开发,要对问题进行分析并进行任务分解。
  • Test-Driven Design,测试保护下的设计改善。TDD 并不能直接提高设计能力,它只是给你更多机会和保障去改善设计。

3、TDD 流程

TDD 的基本流程是:红,绿,重构。
tdd2

更详细的流程是:
tdd1

1、编写测试
2、运行测试,观察测试结果是否如期失败(变红)
3、测试结果不如期失败,返回第1步修改测试
4、测试结果如期失败,编写刚好能够让测试通过的产品代码实现
5、运行测试,观察测试结果是否如期成功(变绿)
6、测试结果不如期成功,返回第4步修改实现
7、测试结果如期成功,分析代码是否需要重构
8、需要重构,返回第4步修改实现
9、不需要重构,编写下一个测试

4、TDD 优点

1、降低开发者负担
通过明确的流程,让我们一次只关注一个点,思维负担更小。

2、保护网
TDD 的好处是覆盖完全的单元测试,对产品代码提供了一个保护网,让我们可以轻松地迎接需求变化或改善代码的设计。
所以如果你的项目需求稳定,一次性做完,后续没有任何改动的话,能享受到 TDD 的好处就比较少了。

3、提前澄清需求
先写测试可以帮助我们去思考需求,并提前澄清需求细节,而不是代码写到一半才发现不明确的需求。

4、快速反馈
有很多人说 TDD 时,我的代码量增加了,所以开发效率降低了。但是,如果没有单元测试,你就要手工测试,你要花很多时间去准备数据,启动应用,跳转界面等,反馈是很慢的。准确说,快速反馈是单元测试的好处。

5、TDD 的三条规则

1、除非是为了使一个失败的 unit test 通过,否则不允许编写任何产品代码

2、在一个单元测试中,只允许编写刚好能够导致失败的内容(编译错误也算失败)

3、只允许编写刚好能够使一个失败的 unit test 通过的产品代码

如果违反了会怎么样呢?
1、违反第一条,先编写了产品代码,那这段代码是为了实现什么需求呢?怎么确保它真的实现了呢?
2、违反第二条,写了多个失败的测试,如果测试长时间不能通过,会增加开发者的压力,另外,测试可能会被重构,这时会增加测试的修改成本。
3、违反第三条,产品代码实现了超出当前测试的功能,那么这部分代码就没有测试的保护,不知道是否正确,需要手工测试。可能这是不存在的需求,那就凭空增加了代码的复杂性。如果是存在的需求,那后面的测试写出来就会直接通过,破坏了 TDD 的节奏感。

我认为它的本质是:
1、分离关注点,一次只戴一顶帽子
在我们编程的过程中,有几个关注点:需求,实现,设计。
TDD 给了我们明确的三个步骤,每个步骤关注一个方面。

  • 红:
    写一个失败的测试,它是对一个小需求的描述,只需要关心输入输出,这个时候根本不用关心如何实现。
  • 绿:
    专注在用最快的方式实现当前这个小需求,不用关心其他需求,也不要管代码的质量多么惨不忍睹。
  • 重构:
    既不用思考需求,也没有实现的压力,只需要找出代码中的坏味道,并用一个手法消除它,让代码变成整洁的代码。

2、注意力控制
人的注意力既可以主动控制,也会被被动吸引。注意力来回切换的话,就会消耗更多精力,思考也会不那么完整。
使用 TDD 开发,我们要主动去控制注意力,写测试的时候,发现一个类没有定义,IDE 提示编译错误,这时候你如果去创建这个类,你的注意力就不在需求上了,已经切换到了实现上,我们应该专注地写完这个测试,思考它是否表达了需求,确定无误后再开始去消除编译错误。

参考:https://www.jianshu.com/p/62f16cd4fef3

go:goroutine、channel、反射

1、goroutine

1、基本介绍

1、Go协程和Go主线程

  • Go主线程(有的程序员直接成为线程/也可以理解成进程):一个Go主线程上,可以起多个协程,即协程是轻量级的线程

2、Go协程的特点

  • 有独立的栈空间
  • 共享程序堆空间
  • 调度由用户控制
  • 协程是轻量级的线程

3、案例
1)在主线程(可以理解成进程),开启一个goroutine,该协程每隔一秒输出“hello world”
2)在主线程中也每隔一秒输出“hello world”,输出10次后,退出程序
3)要求主线程和goroutine同时执行
4)主线程和协程执行流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import (    
"fmt"    
"strconv"   
"time"
)

//编写一个函数,每隔1秒输出“hello world”
func test(){   
for i:=1;i<10 ;i++  {   
fmt.Println("test() hello world"+strconv.Itoa(i))     

time.Sleep(time.Second)  //休眠一秒  
}
}

func main(){  
go test()//开启一个协程    

for i:=1;i<10 ;i++  {    
fmt.Println("main() hello world"+strconv.Itoa(i))      

time.Sleep(time.Second)     //休眠一秒  
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
main() hello world1
test() hello world1
main() hello world2
test() hello world2
test() hello world3
main() hello world3
main() hello world4
test() hello world4
test() hello world5
main() hello world5
test() hello world6
main() hello world6
main() hello world7
test() hello world7
test() hello world8
main() hello world8
main() hello world9
test() hello world9

由输出看出,主线程main和协程test同时执行

  • 主线程和协程执行流程图
    goroutine1

  • 程序开始执行,main主线程开始执行

  • go test()开启协程,主线程main和协程test同时执行
  • 程序退出以主线程为主:
    1) 如果主线程退出了,则协程即使还没有执行完毕也会退出
    2)当然协程可以在主线程没有退出前,就执行完毕退出协程
    

4、总结

  • 1)主线程是一个物理线程直接作用在CPU上。是重量级的,非常耗费CPU资源。

  • 2)协程从主线程开启的,是轻量级的线程,是逻辑态的。对资源消耗相对小

  • 3)Go的协程机制是重要特点,可以轻松的开启上万个协程。(其他编程语言的开发机制一般是基于线程的,开启过多的线程,资源消耗大)

2、goroutine的调度模型

1、MPG模式基本介绍

  • M:操作系统的主线程(是物理线程,真正干活的人)
  • P:协程执行需要的上下文环境(运行时需要的资源和运行时的状态)
  • G:协程(逻辑态的)

2、MPG模式运行的状态

1)MPG模式运行的状态1
goroutine2

2)MPG模式运行的状态2
goroutine3

3、设置运行CPU数目

1
2
3
4
5
6
7
8
9
10
11
12
package main
import "runtime"

func main()
//runtime.NumCPU()   查询系统的CPU数目
cpuNum := runtime.NumCPU()   
println("cpuNum=",cpuNum)     

//设置使用多个CPU     
runtime.GOMAXPROCS(cpuNum-1)     
println("ok")
}

2、管道channel

案例:计算1-20的各个数的阶乘,并且把各个数放入map中并打印,使用goroutine完成

思路:

  • 1、编写一个函数,计算各个数的阶乘并放入map中
  • 2、启动多个协程,统计的结果放入map中
  • 3、map应该做全局的

解法一:使用全局变量加锁同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package main

import (     
"fmt"   
"sync"   
"time"
)

var(  
myMap =make(map[int]int,10)
//全局资源不加锁,会发生资源竞争,同时写会报错   

//可以声明一个全局互斥锁解决    
//sync是一个包,synchornized 同步    
//Mutex 互斥      
lock sync.Mutex
)

//test函数计算n的阶乘,将结果放入myMap中
func test(n int){    
res:=1    
for i:=1;i<=n;i++{      
res*=i  
}    

//写入map前加锁    
lock.Lock()     

//把结果放入myMap     
myMap[n]=res    

//写完map后解锁    
lock.Unlock()
}

func main() {  
//这里开启20个协程,
//20个协程同时向map写数据,会发生 并发map写 错误    
for i:=1;i<=20;i++{         
go test(i)    
}   

//休眠5秒钟(人为估算),让主线程等待所有的协程执行完   
time.Sleep(5*time.Second)   
//如果不休眠,可能main主线程已经结束退出,但是test协程还没写入map     

//互斥性资源读写都要加锁     
lock.Lock()     

//输出结果,对map进行读操作     
for i,v :=range myMap{           
fmt.Printf("map[%d]=%d\n",i,v)   
}    

lock.Unlock()}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
map[17]=355687428096000
map[16]=20922789888000
map[20]=2432902008176640000
map[9]=362880
map[10]=3628800
map[12]=479001600
map[1]=1
map[3]=6
map[19]=121645100408832000
map[11]=39916800
map[15]=1307674368000
map[18]=6402373705728000
map[8]=40320
map[5]=120
map[7]=5040
map[14]=87178291200
map[13]=6227020800
map[2]=2
map[4]=24
map[6]=720

因为map的key是无序的,所以未按递增顺序执行,而且并发执行的顺序也可能不是递增得到结果的

上面的解法中

  • 主线程等待所有goroutine全部完成时间很难确定,这里设置为5秒,是为估算
  • 如果主线程休眠时间长了,会加长等待时间;如果等待时间短了,可能还有goroutine处于工作状态(没有执行完),这时也会随主线程的退出二销毁
  • 通过全局变量加锁同步来实现协程间通讯,并不利于多个协程对全局变量的读写操作

综上,我们引出一种新的通讯机制channel

解法二:使用channel

1、基本介绍

1、channel本质上就是一个数据结构-队列

2、数据先进先出

3、线程安全,多个goroutine访问时,自己不需要再加锁,即:channel本身就是线程安全的

4、channel是有类型的,一个string的channel只能存放string类型数据
channel1

2、基本使用

1、定义/声明
1
2
3
4
5
6
7
8
9
10
11
var 变量名 chan 数据类型

//例
var intChan chan int //intChan类型为int,只能存放int型数据

var mapChan chan map[int]string
//mapChan类型为map[int]string,只能存放map[int]string型数据

var perChan chan Person

var perChan2 chan *Person

2、注意

  • channel是引用类型
  • channel必须初始化后才能写入数据,即make后才能使用
  • channel是由类型的,如intChan类型为int,只能存放int型数据
3、管道的初始化、从管道读写数据以及注意事项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main
import "fmt"

func main() {   
//1、创建一个可以存放3个int的管道   
var intChan chan int    
intChan=make(chan int,3)  //channel make后才能使用

//2、intChan是什么     
fmt.Printf("intChan的值=%v intChan本身的地址=%v\n",intChan,&intChan)   
//输出:intChan的值=0xc000092080
//intChan本身的地址=0xc00008c018      
//可以看出intChan的值为一个地址,所以channel是引用类型。    

//3、向管道写入数据     
intChan<-10//直接写入常量   
num:=211     
intChan<-num//写入变量    

//4、看看管道的长度和容量    
//容量是make时传入的,这里传入的是3,容量不能自动增长,和slice、map不一样    
fmt.Printf("channel len=%v cap=%v\n",len(intChan),cap(intChan))     
//channel len=2 cap=3    

//5、注意:给管道写入数据时不能超过容量     
intChan<-50      //intChan<-98     
fmt.Printf("channel len=%v cap=%v\n",len(intChan),cap(intChan))   
//fatal error: all goroutines are asleep - deadlock!   
//报告死锁deadlock错误     

//6、从管道中读取数据     
var num2 int     
num2 = <-intChan  
fmt.Println("num2=",num2)//num2= 10,从队列头开始取     
fmt.Printf("channel len=%v cap=%v\n",len(intChan),cap(intChan))    
//channel len=2 cap=3     

//7、在没有使用协程的情况下,如果管道中的数据已经全部取出,再取数据就会报告死锁deadlock错误     
num3 := <-intChan    
num4 := <-intChan     
num5 := <-intChan   
fmt.Println("num3=",num3,"num4=",num4,"num5=",num5)   
//fatal error: all goroutines are asleep - deadlock!
}

注意事项

  • channel中只能存放指定的数据类型
  • channel的数据放满后,就不能再放入了,否则报告死锁deadlock错误
  • 当 channel的数据放满后,如果从channel中取出数据,可以再次放入数据
  • 在没有使用协程的情况下,如果channel中的数据取完了,再次取数据,会报告死锁deadlock错误

一个例子,当管道是空接口interface{}类型时,可以存放任意数据类型的值,取出管道中的值对象的的字段值时,需要类型断言

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main
import "fmt"

type Cat struct
Name string     
age int
}

func main() {  
allChan:=make(chan interface{},3)//定义一个空接口类型,容量为3的管道allChan      

allChan<-10//往管道写入int类型的值     
allChan<-"tom"//往管道写入string类型的值    

cat:=Cat{"小花猫",4}//实例化一个Cat    
allChan<-cat////往管道写入Cat类型的值    

//想要取出管道中的第三个值,需要丢弃第一个和第二个    
<-allChan    
<-allChan    

//取到第三个   
newCat:=<-allChan   

//newCat从编译层面看是空接口类型,接口类型本身没有字段    
//但在运行时可动态指向结构体Cat,
//下面写法语法上没错,运行时可动态指向Cat     
fmt.Printf("newCat的类型=%T newCat的值=%v\n",newCat,newCat)   
//输出:newCat的类型=main.Cat newCat的值={小花猫 4}

//newCat从编译层面看是空接口类型,接口类型本身没有字段,
//直接写编译不通过,需要类型断言      
aNewCat:=newCat.(Cat)     
fmt.Printf("newCat.Name=%v",aNewCat.Name)
//输出:newCat.Name=小花猫
}

4、管道的遍历和关闭

1、管道的关闭

使用内置函数close()可以关闭管道,当管道关闭后,就不能再向管道写数据了,但是仍然可以从管道中读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import "fmt"

func main() {
intChan:=make(chan int,3)  
intChan<-100   
intChan<-200    

//1、关闭管道    
close(intChan)    

//2、关闭管道后,不能再写入数据   
//intChan<-300    
//panic: send on closed channel     
//向一个关闭的通道中发送数据,panic终止程序    

//3、关闭管道后,可以再读取数据   
n1:=<-intChan    
fmt.Println("n1=",n1)//n1= 100
}

2、管道的遍历
channel支持for-range的方式进行遍历,有两个细节注意:

  • 在遍历时,如果channel没有关闭,则会出现死锁deadlock错误
  • 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历

在遍历时不能使用普通for循环遍历,因为管道的长度(len(intChan))是随数据出管道-1动态变化的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main
import "fmt"

func main() {   
intChan:=make(chan int,100)    

//循环往管道中写入100个数据      
for i:=0;i<100 ;i++  {       
intChan<-i*2   
}    

//遍历,一定要先关闭管道,如果不关闭管道,会出现死锁deadlock错误     
close(intChan)    

//管道没有下标,for-range只返回一个值    
for v:= range intChan{         
fmt.Println("v=",v)     
}
}

3、goroutine和管道channel结合使用

1、案例:

案例:
goroutine和管道channel协同工作
1)开启一个wiiteData协程,向管道写入50个整数
2)开启一个readData协程,从管道中读取wiiteData写入的数据
3)注意:wiiteData和readData操作的是同一个管道
4)主线程需要等待wiiteData和readData协程完成工作后才能退出
思路图解:
goandchan1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package main
import "fmt"

//writeData
func writeData(intChan chan int)
for i:=1;i<=50;i++  {   
intChan<-i         
fmt.Println("writeData ",i)    
}      

close(intChan)//关闭intChan,后面intChan仍然可读
}

//readData
func readData(intChan chan int,exitChan chan bool){      
for{         
v,ok:=<-intChan      
if !ok {       
break     
}         
fmt.Printf("readData 读到数据=%v\n",v)   
}    

//readData读取数据完后,    
//往exitChan写入数据true,告知主线程readData执行完毕    
exitChan<-true   
close(exitChan)
}

func main() {    
//创建两个管道  
intChan:=make(chan int,50)  
exitChan:=make(chan bool,1)  //该管道用于等到intChan管道的读取协程工作完毕后,往exitChan写入数据,标识主线程可退出

go writeData(intChan) //向管道写入50个整数
go readData(intChan,exitChan)   //从管道中读取wiiteData写入的数据

for{      
_,ok:=<-exitChan    
if !ok {            
break       
}   
}
}

2、管道阻塞机制

如果只向管道里写数据,而没有读取,就会出现阻塞而死锁deadlock。
注意:如果有向管道读取数据,但读取比写数据慢得多,也不会发生死锁,只要编译器检测到数据在管道中是流动的,即有读取也有写入,那么就不会发生死锁

下面例子中,intChan容量改为10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package main
import "fmt"

//writeData
func writeData(intChan chan int)
for i:=1;i<=50;i++  {   
intChan<-i     //数据一下就放入,但下面readData读取慢慢读
    
fmt.Println("writeData ",i)    
}      

close(intChan)//关闭intChan,后面intChan仍然可读
}

//readData
func readData(intChan chan int,exitChan chan bool){      
for{         
v,ok:=<-intChan      
if !ok {       
break     
}         
time.Sleep(time.Second)//慢慢读,每隔一秒读一次

fmt.Printf("readData 读到数据=%v\n",v)   
}    

//readData读取数据完后,    
//往exitChan写入数据true,告知主线程readData执行完毕    
exitChan<-true   
close(exitChan)
}

func main() {    
//创建两个管道  
intChan:=make(chan int,10)//容量10
exitChan:=make(chan bool,1)  //该管道用于等到intChan管道的读取协程工作完毕后,往exitChan写入数据,标识主线程可退出

go writeData(intChan) //向管道写入50个整数
go readData(intChan,exitChan)   //从管道中读取wiiteData写入的数据

for{      
_,ok:=<-exitChan    
if !ok {            
break       
}   
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
writeData  1
writeData 2
writeData 3
writeData 4
writeData 5
writeData 6
writeData 7
writeData 8
writeData 9
writeData 10
writeData 11
readData 读到数据=1
writeData 12
readData 读到数据=2
writeData 13
readData 读到数据=3
writeData 14
readData 读到数据=4
writeData 15
readData 读到数据=5
writeData 16
readData 读到数据=6
writeData 17
readData 读到数据=7
writeData 18
readData 读到数据=8
writeData 19
readData 读到数据=9
writeData 20
readData 读到数据=10
writeData 21
readData 读到数据=11
writeData 22
readData 读到数据=12
writeData 23
readData 读到数据=13
writeData 24
readData 读到数据=14
writeData 25
readData 读到数据=15
writeData 26
readData 读到数据=16
writeData 27
readData 读到数据=17
writeData 28
readData 读到数据=18
writeData 29
readData 读到数据=19
writeData 30
readData 读到数据=20
writeData 31
readData 读到数据=21
writeData 32
readData 读到数据=22
writeData 33
readData 读到数据=23
writeData 34
readData 读到数据=24
writeData 35
readData 读到数据=25
writeData 36
readData 读到数据=26
writeData 37
readData 读到数据=27
writeData 38
readData 读到数据=28
writeData 39
readData 读到数据=29
writeData 40
readData 读到数据=30
writeData 41
readData 读到数据=31
writeData 42
readData 读到数据=32
writeData 43
readData 读到数据=33
writeData 44
readData 读到数据=34
writeData 45
readData 读到数据=35
writeData 46
readData 读到数据=36
writeData 47
readData 读到数据=37
writeData 48
readData 读到数据=38
writeData 49
readData 读到数据=39
writeData 50
readData 读到数据=40
readData 读到数据=41
readData 读到数据=42
readData 读到数据=43
readData 读到数据=44
readData 读到数据=45
readData 读到数据=46
readData 读到数据=47
readData 读到数据=48
readData 读到数据=49
readData 读到数据=50

因为管道容量为10,所以写到11的时候,需要等待数据被读出才能写入,所以写数据12-50与读取是交错的,读取数据每隔一秒读取一次。最后数据全部写完,只需要读取数据。

3、细节

1、在默认情况下,管道是双向的,即可读可写
2、管道可以声明为只读或只写

1
2
3
4
5
6
7
8
9
10
11
//管道声明为只写
var chan1 chan<- int
chan1 = make(chan int,3)
chan1 <- 1 //ok
num1 := <-chan1 //error,无效的操作

//管道声明为只读
var chan2 <-chan int
chan2 = make(chan int,3)
num2 := <-chan2 //ok
chan2 <- 2 //error

管道声明为只读或只写的应用场景:

  • 先声明一个双向管道intChan
  • 声明两个函数,一个往intChan只写数据的send函数,一个往intChan只读数据的recv函数,防止误操作
  • 可以将双向管道intChan作为实参,传给只写数据的send函数,send函数形参为ch chan<- int
  • 可以将双向管道intChan作为实参,传给只读数据的recv函数,re函数形参为ch <-chan int
  • 上面所述中,将双向管道作为实参传给单向管道(只读或只写)并不会报错。
  • 管道的双向和单向只是管道的性质,但是管道的类型都是chan int,所以不会报错

3、使用select可以解决从管道取数据的阻塞问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
import (    
"fmt"
)

func main() {   
//使用select可以解决从管道取数据的阻塞问题     

//1、定义一个管道 10个数据 int     
intChan := make(chan int,10)    
for i:=0;i<10;i++  {     
intChan<-i     
}     

//2、定义一个管道 5个数据,string     
stringChan := make(chan string,5)    
for i:=0;i<5 ;i++  {   
stringChan<-"hello"+fmt.Sprintf("%d",i)  
}   

//传统方法在遍历管道时,如果不关闭会阻塞并死锁deadlock   
//但在实际开发中,我们往往不确定关闭管道的时机    

//由此我们使用select解决      
for{          
select {          
//重点:这里如果intChan一直没有关闭,不会一直阻塞而死锁,                
//会自动到下一个case匹配         
case v:=<-intChan:              
fmt.Printf("从intChan读取了数据%d\n",v)         
case v:=<-stringChan:                   
fmt.Printf("从stringChan读取了数据%s\n",v)              
default:                     
fmt.Printf("都取不到了,加入处理逻辑\n")                    
return        
}    
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
从stringChan读取了数据hello0
从intChan读取了数据0
从stringChan读取了数据hello1
从stringChan读取了数据hello2
从stringChan读取了数据hello3
从intChan读取了数据1
从intChan读取了数据2
从stringChan读取了数据hello4
从intChan读取了数据3
从intChan读取了数据4
从intChan读取了数据5
从intChan读取了数据6
从intChan读取了数据7
从intChan读取了数据8
从intChan读取了数据9
都取不到了,加入处理逻辑

4、goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

  • 如果我们起了一个协程,但是这个协程出现了panic,如果我们没有捕获这个panic,就会造成成哥程序的崩溃,

  • 这时可以在该协程中使用recover来捕获panic进行处理。这样即使这个协程发生问题,主线程仍然不受影响,继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main
import (    
"fmt"   
"time"
)

//函数sayHello
func sayHello(){  
for i:=0;i<10 ;i++  {           
time.Sleep(time.Second)    
fmt.Println("hello world")  
}
}

//函数test
func test(){   
//这里使用defer+recover解决panic终止程序    
defer func() {           
//捕获test抛出的panic         
if err:=recover();err!=nil {            
fmt.Println("test() 发生错误")   
}    
}()     

//定义一个map  
var myMap map[int]string     
myMap[0]="golang"//空map直接赋值,报错
}

func main() {   
go sayHello()    
go test()   //这个协程会panic 使用defer+recover解决

for i:=0;i<10 ;i++  {       
time.Sleep(time.Second)       
fmt.Println("main() ok=",i)   
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
test() 发生错误
hello world
main() ok= 0
main() ok= 1
hello world
hello world
main() ok= 2
hello world
main() ok= 3
hello world
main() ok= 4
main() ok= 5
hello world
hello world
main() ok= 6
main() ok= 7
hello world
hello world
main() ok= 8
main() ok= 9

由输出看出,test协程出错,但是主线程和sayHello协程继续执行

4、反射

1、基本介绍

1、反射的作用:

  • 1)、反射可以在运行时动态获取变量的各种信息,如变量的类型(type)、类别(kind)

  • 2)、如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段、方法)

  • 3)、通过反射,可以修改变量的值,可以调用关联的方法

  • 4)、使用反射,需要import(“reflect”)

reflect实现了运行时反射,允许程序操作任意类型的对象,典型用法是用静态类型interface{}保存一个值,通过调用TypeOf获取类型信息,该函数返回一个Type类型值,调用ValueOf函数返回一个value类型值,该值代表运行时数据,Zero接受一个Type类型参数并返回该类型零值的value类型值。

  • 5)、反射示意图

reflect1

reflect.Type是一个接口,定义了非常多方法,通过这些方法可以反向操作变量,获取变量的各种信息

reflect.Value是一个结构体,包含了非常多方法,可通过Type()方法将Value转换为Type、返回变量的字段和方法等等

  • 6)、变量、interface{}和reflect.Value是可以相互转换的
    reflect2

2、案例

1、对基本数据类型、interface{}、reflect.Value进行反射的基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main
import (     
"fmt"   
"reflect"
)

//反射
func reflectTest01(b interface{}){  
//通过反射获取传入变量的type、kind、值     

//1、先获取到reflect.Type     
rTyp := reflect.TypeOf(b)     
fmt.Println("rTyp=",rTyp)     

//2、获取到reflect.Value     
rVal := reflect.ValueOf(b)//rVal=100      
fmt.Printf("rVal=%v rVal的type=%T\n",rVal,rVal)     
//n:=rVal+2
//error,rVal的类型不是int,是reflect.Value,不能做运算    

//3、如果要做运算,如下      
n:=rVal.Int()+2     
fmt.Println("n=",n)    

//4、将rval转成interface{}      
iv:=rVal.Interface()     

//5、将interface{}通过断言转成int(对应的类型)    
num2:=iv.(int)     
fmt.Printf("num2=%v num2的type=%T\n",num2,num2)
}

func main() {    
//1、定义一个int     
var num int = 100     

//2、反射
reflectTest01(num)
}

输出:

1
2
3
4
rTyp= int
rVal=100 rVal的type=reflect.Value
n= 102
num2=100 num2的type=int

2、对结构体、interface{}、reflect.Value进行反射的基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main
import (
"fmt"
"reflect"
)

type Student struct {  
Name string     
age int
}

//对结构体反射
func reflectTest02(b interface{}){  
//通过反射获取传入变量的type、kind、值     
//1、先获取到reflect.Type   
rTyp := reflect.TypeOf(b)    
fmt.Println("rTyp=",rTyp)   

//2、获取到reflect.Value     
rVal := reflect.ValueOf(b)   
fmt.Printf("rVal=%v rVal的type=%T\n",rVal,rVal)    

//3、获取变量对应的kind,两种方式等价
//(1)通过获取到的reflect.Type来获取 =》rTyp.Kind()
kind1:=rTyp.Kind()
//(2)通过获取到的reflect.Value来获取 =》rVal.Kind()
kind2:=rVal.Kind()
fmt.Printf("kind1=%v kind2=%v\n",kind1,kind2)

//3、将rval转成interface{}     
iv:=rVal.Interface()     
fmt.Printf("iv=%v iv的type=%T\n",iv,iv)  
//输出:iv={tom 20} iv的type=main.Student   

//4、iv的type=main.Student,但是通过iv取字段会报错     
//iv.Name     
//error 因为编译器在编译阶段无法知道iv的类型是main.Student,只有运行时才知道     
//所以这里直接编译报错    
//反射是在程序运行时工作的    

//5、将interface{}通过断言转成int(对应的类型)    
stu:=iv.(Student)    
if ok {     
fmt.Printf("stu=%v stu的type=%T\n",stu,stu)    
fmt.Printf("stu.Name=%v stu.Age=%v\n",stu.Name,stu.Age)
}
}

func main() {  
//1、定义一个student实例   
stu:=Student{"tom",20}     
//2、反射   
reflectTest02(stu)
}

输出:

1
2
3
4
5
6
rTyp= main.Student
rVal={tom 20} rVal的type=reflect.Value
kind1=struct kind2=struct
iv={tom 20} iv的type=main.Student
stu={tom 20} stu的type=main.Student
stu.Name=tom stu.Age=20

3、注意事项

1、reflect.Value.Kind(),获取变量的类别,返回一个常量。type是一个大范畴,kind在type上细分(如type只返回int,kind返回具体的int32,int64的常量定义的值等)
在上面的例子中有:

1
2
3
4
5
6
//3、获取变量对应的kind,两种方式等价
//(1)通过获取到的reflect.Type来获取 =》rTyp.Kind()
kind1:=rTyp.Kind()
//(2)通过获取到的reflect.Value来获取 =》rVal.Kind()
kind2:=rVal.Kind()
fmt.Printf("kind1=%v kind2=%v\n",kind1,kind2)

输出:

1
kind1=struct kind2=struct

2、Type和Kind的区别:Type是类型,Kind是类别,Type和Kind可能相同也可能不同

  • var num int = 10 num的Type是int,Kind也是int
  • var stu Student stu的Type是包名.Student,Kind是struct

3、变量、interface{}和reflect.Value是可以相互转换的

4、使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,如x是int,那么就应该用reflect.ValueOf(x).Int(),不能使用其他,否则panic

5、通过反射修改变量的值,注意当使用SetXXX方法来修改变量的值,需要通过变量对应的指针来修改,这时需要使用reflect.Value.Elem()方法

  • func (v Value) SetXXX(x XXX)

    1
    2
     func (v Value) SetXXX(x XXX) 
    //SetXXX方法是和Value类型绑定的方法,不能用指针类型去调用它,只能把指针类型再转为Value类型,使用Elem函数
  • func (v Value) Elem() Value

    1
    2
    func (v Value) Elem() Value
    //重点:Elem返回v持有的接口保管的值的Value类型封装,或持有的指针指向的值的Value类型封装

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main
import (    
"fmt"    
"reflect"
)

//修改反射变量的值    
func reflectTest01(b interface{}){    
//1、获取到reflect.Value  
rVal := reflect.ValueOf(b)//rVal=100    
fmt.Printf("rVal=%v rVal的type=%T\n",rVal,rVal)  
//main函数中调用函数时传的是地址,rVal的值为一个地址

//2、获取rVal的Kind    
rKind:=rVal.Kind()  
fmt.Printf("rVal Kind=%v\n",rKind)   
//输出:rVal Kind=ptr,
//rVal的Kind是指针,因为main函数中调用函数时传的是地址     

//3、通过SetXXX修改变量的值    
//rVal.SetInt(20) //error   

//func (v Value) SetXXX(x XXX),  
//SetXXX方法是和Value类型绑定的方法,不能用指针类型去调用它    
//只能把指针类型再转为Value类型,使用Elem函数    

//func (v Value) Elem() Value   
//Elem返回v持有的接口保管的值的Value类型封装,或持有的指针指向的值的Value类型封装     

//4、通过Elem()获取到指针指向的值,再通过SetXXX修改变量的值     
rVal.Elem().SetInt(20)    //值修改为20
}

func main() {     
//1、定义一个int    
var num int = 100     
//2、反射    
reflectTest01(&num)//修改变量num的值,所以要穿num的地址

//3、打印修改后的num
fmt.Println("num=",num)
}

输出:

1
2
3
rVal=0xc000060058 rVal的type=reflect.Value
rVal Kind=ptr
num= 20

go:文件操作、json

1、文件操作

1、基本认识

1、文件在程序中是以 的形式来操作的

  • 流:数据在数据源(文件)和程序(内存)之间经历的路径
  • 输入流:数据从数据源(文件)到程序(内存)之间的路径
  • 输出流:数据从程序(内存)到数据源(文件)之间的路径

file1

2、GO中,os.File封装所有文件相关操作(方法),File是一个结构体

3、文件是引用类型

2、文件操作

1、打开文件
1
func Open(path string) (file *File,err error)

Open打开一个文件用于读取,如果操作成功,返回一个文件对象(结构体指针或者文件句柄)可用于读取数据;如果出错,错误底层类型是PathError

2、关闭文件
1
func (f *File ) Close (error) error

Close关闭文件f,用于读取,使文件不能用于读写。返回可能出现的错误

案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main(){
//打开文件
file , err := os.Open("d:/test.txt")
if err != nil {
fmt.Println("open file err=",err)
}

//输出文件
fmt.Printf("file=%v",file)//输出 file=&<0xcXXXXXXXX>,
//看出文件是一个地址(指针)

//关闭文件
err = file.Close()
if err != nil {
fmt.Println("close file err=",err)
}
}

3、读文件操作应用实例

1、读取文件的内容并显示在终端(带缓冲区的方式)
os.Open()、 file.Close()、bufio.NewReader()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func main(){
//打开文件
file , err := os.Open("d:/test.txt")
if err != nil {
fmt.Println("open file err=",err)
}

//当函数退出时,要及时关闭文件
defer file.Close() //及时关闭file句柄,防止内存泄漏

//创建一个 *Reader,是带缓冲的
//默认缓冲4096个字节
reader := bufio.NewReader(file)

//循环读取文件的内容
for{
str ,err := reader.ReadString("\n")//读到一个换行就结束一次
if err == io.EOF {//io.EOF表示文件的末尾
break
}

//输出内容
fmt.Print(str)
}
//文件读取结束
fmt.Println("文件读取结束")
}

2、读取文件的内容并显示在终端(使用ioutil一次性将整个文件读入到内存中),这种方式适合文件不大的情况 。

  • 这种方式不用显示的Open和Close文件,因为文件的Open和Close被封装到ReadFile函数内部
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main(){
//打开文件
file = "d:/test.txt"

//使用ioutil.ReadFile一次性将文件读取完
content,err := ioutil.ReadFile(file)//返回一个[]byte(byt切片)
if err != nil {
fmt.Println("read file err=",err)
}

//把读取的文件内容显示到终端
fmt.Printf("content=%v",content)//这是一个[]byte

fmt.Printf("content=%v",string(content))//[]byte转成string输出

}
4、写文件操作应用实例

1、基本介绍

1
func OpenFile(path string,flag int,perm FileMode) (file *File,err error)

  • path:要打开文件的路径
  • flag:打开文件的方式
  • perm:文件权限,Windows系统下无效,Unix和Linux下有效
1、实例一

带缓冲bufio.NewWriter

1)创建一个新文件写入内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func main(){
filePath := "d:\abc.test"
//打开文件
file , err := os.OpenFile(filePath,os,O_WRONLY | os.O_CREATE,0666)
//以只写方式打开,如果没有文件就创建一个,如果有就继续往下

if err != nil {
fmt.Println("open file err=",err)
return
}

//当main函数退出之前,要及时关闭文件
defer file.Close() //及时关闭file句柄,防止内存泄漏


//写入内容
str := "要写入的内容\r\n"
//有的编辑器\n会换行
//有的编辑器\r才会换行(如记事本)

//写入时,使用带缓冲的*Writer
writer := bufio.NewWriter(file)

//循环写入内容
for i:=0;i<5;i++{//写5次
writer.writerString("str")
}

//因为Writer是带缓存的,
//因此在调用writerString方法时,内容是先写入缓存的,
//所以需要调用Flush方法,将缓冲的数据真正写入到文件中,
//否则文件中会没有数据
writer.Flush()
}

2)打开一个已经存在的文件,将原来的内容覆盖成新的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func main(){
filePath := "d:\abc.test"
//打开文件
file , err := os.OpenFile(filePath,os,O_WRONLY | os.O_TRUNC,0666)
//以只写方式打开,如果文件已经有内容就清空

if err != nil {
fmt.Println("open file err=",err)
return
}

//当main函数退出之前,要及时关闭文件
defer file.Close() //及时关闭file句柄,防止内存泄漏


//写入要覆盖的新内容
str := "写入要覆盖的新内容\r\n"
//有的编辑器\n会换行
//有的编辑器\r才会换行(如记事本)

//写入时,使用带缓冲的*Writer
writer := bufio.NewWriter(file)

//循环写入内容
for i:=0;i<10;i++{//写5次
writer.writerString("str")
}

//因为Writer是带缓存的,
//因此在调用writerString方法时,内容是先写入缓存的,
//所以需要调用Flush方法,将缓冲的数据真正写入到文件中,
//否则文件中会没有数据
writer.Flush()
}

3)打开一个已经存在的文件,在原来的内容追加新的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func main(){
filePath := "d:\abc.test"
//打开文件
file , err := os.OpenFile(filePath,os,O_WRONLY | os.O_APPEND,0666)
//以只写方式打开,在文件末尾以追加的形式写入内容

if err != nil {
fmt.Println("open file err=",err)
return
}

//当main函数退出之前,要及时关闭文件
defer file.Close() //及时关闭file句柄,防止内存泄漏


//写入要追加写入的新内容
str := "追加写入的新内容\r\n"
//有的编辑器\n会换行
//有的编辑器\r才会换行(如记事本)

//写入时,使用带缓冲的*Writer
writer := bufio.NewWriter(file)

//循环写入内容
for i:=0;i<10;i++{//写5次
writer.writerString("str")
}

//因为Writer是带缓存的,
//因此在调用writerString方法时,内容是先写入缓存的,
//所以需要调用Flush方法,将缓冲的数据真正写入到文件中,
//否则文件中会没有数据
writer.Flush()
}

4)打开一个已经存在的文件,将原来的内容读出显示在终端,并追加新的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func main(){
filePath := "d:\abc.test"
//打开文件
file , err := os.OpenFile(filePath,os,O_RDWR | os.O_APPEND,0666)
//以读写方式打开,在文件末尾以追加的形式写入内容

if err != nil {
fmt.Println("open file err=",err)
return
}

//当main函数退出之前,要及时关闭文件
defer file.Close() //及时关闭file句柄,防止内存泄漏

//先读取文件原来的内容,并显示在终端
//创建一个 *Reader,是带缓冲的
//默认缓冲4096个字节
reader := bufio.NewReader(file)

//循环读取文件的内容
for{
str ,err := reader.ReadString("\n")//读到一个换行就结束一次
if err == io.EOF {//io.EOF表示文件的末尾
break
}

//输出内容,显示在终端
fmt.Print(str)
}

//写入要追加写入的新内容
str := "第二次追加写入的新内容\r\n"
//有的编辑器\n会换行
//有的编辑器\r才会换行(如记事本)

//写入时,使用带缓冲的*Writer
writer := bufio.NewWriter(file)

//循环写入内容
for i:=0;i<5;i++{//写5次
writer.writerString("str")
}

//因为Writer是带缓存的,
//因此在调用writerString方法时,内容是先写入缓存的,
//所以需要调用Flush方法,将缓冲的数据真正写入到文件中,
//否则文件中会没有数据
writer.Flush()
}

2、实例二

将一个文件的内容,写入到另外一个文件。(两个文件已经存在)
ioutil.ReadFile/ioutil.WriteFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main(){
//将”d:/test.txt“文件内容导入到”e:/kkk.txt“

//1、将”d:/test.txt“文件内容读取到内存
//2、将读取到的内容写入”e:/kkk.txt“

file1Path = "d:/test.txt"
file2Path = "e:/kkk.txt"

//使用ioutil.ReadFile一次性将文件读取完
data,err := ioutil.ReadFile(file1Path)//返回一个[]byte(byt切片)
if err != nil {
fmt.Println("read file err=",err)
return
}

//把读取的文件内容显写入e:/kkk.txt文件
err = ioutil.WriteFile(file2Path,data,0666)//data接收一个[]byte
if err != nil {
fmt.Println("write file err=",err)
return
}
}

5、判断文件或文件夹是否存在
1
func Stat(filePath string) (fi FileInfo,err error)
  • 如果返回的error为nil,说明文件或文件夹存在
  • 如果返回的error使用为os.IsNotExist判断为true,说明文件或文件夹不存在
  • 如果返回的error为其他类型,则不确定是否存在

2、json

1、JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。易于人阅读和编写,也易于机器解析和生成,并有效提升网络效率。

2、json序列化(如struct、map、slice序列化成json字符串)
json.Marshal()序列化后是一个[]byte,要转string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main
import (
"encoding/json"
"fmt"
)

type Monster struct {
Name string
Age int   
Birthday  string   
Sal float64   
Skill string
}

func testStruct(){
monster := Monster{
Name:"牛魔王",            
Age:500,            
Birthday:"2011-11-11",           
Sal:8000.0,            
Skill:"牛魔拳",      
}      

//将monster序列化     
data,err:=json.Marshal(&monster)      
if err!=nil{            
fmt.Printf("序列化失败 err=%v\n",err)     
}     

//输出序列化后的结果     
//json.Marshal(&monster)序列化后是一个[]byte,要转string      
fmt.Printf("序列化后的结果monster=%v",string(data))
}

func main() {
testStruct()
}

输出:

1
序列化后的结果monster={"Name":"牛魔王","Age":500,"Birthday":"2011-11-11","Sal":8000,"Skill":"牛魔拳"}

注意:也可以对基本数据类型进行序列化,序列化结果直接将基本数据类型转成string,但是对基本数据类型进行序列化一般没有意义。

3、json反序列化(如将json反序列化成struct、map、slice)
json.Unmarshal([]byte(str),&monster)第一个参数要传一个[]byte

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main
import (
"encoding/json" 
"fmt"
)
//反序列化

type Monster struct {
Name string    
Age int    
Birthday  string     
Sal float64    
Skill string
}

func unSerialStruct(){  
str:="{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\"," +            "\"Sal\":8000,\"Skill\":\"牛魔拳\"}"     

//定义一个Monster实例      
var monster Monster      

//json.Unmarshal([]byte(str),&monster)第一个参数要传一个[]byte     
err := json.Unmarshal([]byte(str),&monster)
//传地址才能改变var monster的值
      if err!=nil
fmt.Printf("反序列化失败 err=%v\n",err)    
}      

fmt.Printf("反序列化后的结果monster=%v",monster)
}

func main() {   
unSerialStruct()
}

输出:

1
反序列化后的结果monster={牛魔王 500 2011-11-11 8000 牛魔拳}

go:结构体、方法、工厂模式、OOP三大特性、类型断言、接口

面向对象-结构体

1、GO支持面向对象编程(OOP),但不是纯粹的面向对象编程语言,更准确地说是GO支持面向对象编程特性
2、GO没有类的概念,结构体(struct)来实现OOP
3、GO面向对象编程非常简洁,去掉传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等
4、Go仍然有面向对象编程的继承、封装和多态的特性,只是实现方式不一样。
5、GO面向对象(OOP)很优雅,OOP本身就是语言类型系统的一部分(Go天然支持OOP),通过接口(interface)关联,耦合性低,非常灵活。
6、GO更准确的说是面向接口编程

1、结构体变量在内存中的布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个结构体
type Cat struct{
Name string
Age int
Color string
Hobby string
}

//创建一个结构体cat变量
vat cat1 Cat
cat.Name = "小白"
cat.Age = 3
cat.Color = "白色"
cat.Hobby = "吃鱼"

fmt.Println("cat1=",cat1)

结构体是自定义的数据类型(如Cat),结构体变量(实例)是具体的、实际的一个变量(如cat1)

结构体实例在内存中的布局,以cat1为例
1、当代码执行到“var cat1 Cat”时,cat1指向一个结构体,这时还没有给结构体的各个字段赋值,所以各个字段为默认值
struct1

2、结构体有一个自己的地址,cat1直接指向一个结构体的数据空间,而不是结构体的地址,所以结构体是值类型

3、赋值语句为结构体的字段赋值
struct2

总结:

  • 当我们声明一个结构体变量时,结构体的数据空间已经有了,且结构体的每个字段已有默认值
  • 声明结构体变量后,该变量指向一个结构体的数据空间,而不是该结构体数据空间的地址,所以结构体是值类型
2、结构体声明

1、在创建一个变量后,如果没有给字段赋值,系统会赋零值

  • 基本数据类型赋零值
  • 引用类型:slice、map、指针的零值是nil,即没有分配内存空间
  • 数组类型的默认值与元素相关

2、不同结构体变量的字段你是独立的,互不影响。因为结构体是值类型、默认是值拷贝

3、创建结构体实例的4种方式

1、直接声明

1
2
3
4
vat person Person
person.field1 = value1
person.field2 = value2
person.field3 = value3

2、声明时直接为字段赋值

1
2
3
4
5
vat person Person = Person{
field1:value1
field2:value2
field3:value3
}

3、通过new函数创建,创建对象是一个指向该结构体的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
vat person *Person = new(Person)
//person是一个指针,标准赋值方式如下
(*person).field1 = value1
(*person).field2 = value2
(*person).field3 = value3
//运算符优先级:"." > "*",所以需要括号(*person).field1

//可简写为
person.field1 = value1
person.field2 = value2
person.field3 = value3

//GO的设计者为了使用方便,底层会对简写写法person.field1 = value1进行处理,会给person加上取值运算 (*person).field1 = value1

4、vat person *Person = &Person{},可以直接再{}中赋值,如方式2,也可如下赋值,如方式3

1
2
3
4
5
6
7
8
9
10
11
12
13
    vat person *Person = &Person{}

//person是一个指针,标准赋值方式如下
(*person).field1 = value1
(*person).field2 = value2
(*person).field3 = value3

//可简写为
person.field1 = value1
person.field2 = value2
person.field3 = value3

//GO的设计者为了使用方便,底层会对简写写法person.field1 = value1进行处理,会给person加上取值运算 (*person).field1 = value1

总结:

  • 方式3和方式4返回的是一个结构体指针
4、结构体内细节

1、结构体中所有字段在内存中是连续分布的
2、当结构体的字段是指针时,指针本身的地址是连续的,但是指针指向的地址指值不一定连续
3、结构体使用户单独定义的类型,和其它类型转换时,需要有完全相同的字段(名字、个数和累型)
4、结构体进行type重新定义(相当于取别名),Go认为是新的数据类型,但是相互间可以强转
5、在结构体的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Monster struct{
Name string `json:"name"`
Age int `json:"age"`
Skill string `json:"skill"`
}

monster := Monster{
"牛魔王"
500
“牛头拳”
}

//将monster变量序列化为json字符串(反射机制)
jsonStr,err := json.Marshal(monster)//返回值为[]byte切片
//这里json包去访问monster中的字段,如果字段小写则访问不到,字段小写表示只能当前包内访问,则只能大写。但是大写,json格式化后json串中也是大写,与前端命名习惯不一致,所以可以用tag为字段取别名成小写形式

if err != nil{
fmt.Println("json处理错误",err)
}
fmt.Println("jsonStr",string(jsonStr))

方法method

1、Go中方法是作用在指定的数据类型上的(和指定数据类型绑定),因此自定义类型,都可以有方法(不仅是struct)。

2、方法的声明与调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type A struct{
Num int
}

func (a A) test(){//A结构体有一个方法test,说明test方法和结构体(类型)A绑定,test方法只能被A的对象来调用

a.Num=2 //这里的改变不会影响main函数中的赋值输出,因为struct是值类型,func (a A)这里不是指针。
fmt.Println(a.Num)
}

func main(){
var a A
a.Num=1
a.test()//调用方法
}

由上述代码有:

  • 1、test方法和A类型绑定
  • 2、test方法只能通过A的实例来调用,不能直接调用,也不能用其他类型调用
  • 3、func (a A) test(),a表示哪个A实例调用,他就是该实例的副本,和函数传参类似,由于struct是值类型,func (a A)这里不是指针,所以方法中对字段重新赋值不会影响main函数中的值

方法的调用和传参机制

重点:方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的实例变量,当作实参也传递给方法

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Person struct{
Name string
}

func (p Person) getSum(n1 int,n2 int) int{
return n1+n2
}

func main(){
var p Person
p.Name = "tom"

n1 := 10
n2 := 20
res := p.getSum(n1,n2)//调用方法

fmt.Println("res=",res)
}

1、main函数中执行“n1 := 10 n2 := 20”时,内存中开辟main栈并压入变量n1和n2
method1

2、main函数执行 “ res := p.getSum(n1,n2)”时:

  • 先执行“p.getSum(n1,n2)”,内存中开辟getSum栈,并将n1和n2的值拷贝到getSum栈中,同时将结构体实例p值拷贝到getSum栈中(因为结构体实例p调用方法getSum),因为这里传的是“func (p Person)”,所以是值拷贝,所以main栈和getSum栈是完全独立的数据空间。若传指针则为引用传递
    method2
  • 然后执行赋值给res变量“res := p.getSum(n1,n2)”,main栈中压入res变量,res变量的值有函数getSum返回
    method3

注意:如果一个类型(结构体)实现了String()这个方法,fmt.Println默认会调用这个结构体变量的String()方法输出

方法和函数的区别

1、调用方式不一样

  • 函数的调用方式:函数名(实参列表)
  • 方法的调用方式:绑定类型变量.方法名(实参列表)

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

3、对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反之亦然

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
type Person struct{
Name string
}

func (p Person) test() {
p.Name = "jack"
fmt.Println("tes=t",p.Name) //jack
}

func (p *Person) test01() {
p.Name = "marry"
fmt.Println("tes=t",p.Name) //marry
}

func main(){
var p Person
p.Name = "tom"

p.test() //ok
fmt.Println("tes=t",p.Name) //tom

(&p).test() //用p的地址(指针)调也可以,但仍然是值拷贝,只是形式上看是引用拷贝,但是接受的地方“func (p Person)”是传值,所以底层会处理为“p.test()”,只有接受的地方为传地址(如 func (p *Person)),才是引用传递
fmt.Println("tes=t",p.Name) //tom

(&p).test01()//ok
fmt.Println("tes=t",p.Name)//marry

p.test01()//ok,等价于(&p).test01(),底层处理,形式上传值,实际传地址,因为接受的地方为传地址(如 func (p *Person))
fmt.Println("tes=t",p.Name)//marry
}

针对第3条总结重点重点
不管调用的形式如何,真正决定是值拷贝还是地址拷贝,看定义这个方法时跟哪个类型绑定,若跟指针(如 p *Person)绑定则为地址拷贝,若跟值类型(如 p Person)绑定则为值拷贝

工厂模式

1、GO的结构体没有构造函数,通常使用工厂模式来解决

  • 使用工厂模式实现跨包创建结构体实例

1)model包下有结构体Student

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package model 

type student struct{
//这里结构体定义为小写student,只能在当前包内使用,其他包不可访问
Name string
Score float64
}

//使用工厂模式
func NewStudent(name string,score float64) *student{
return &student{
Name : name,
Score : score,
}
}

2)main包下使用工厂模式创建结构体student实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"factory/model"
)

func main(){
//使用工厂模式创建student实例
var stu = model.NewStudent("tom",88.8)//返回值为一个指针

fmt.Println(stu) //输出&{tom 88.8}
fmt.Println(*stu) //输出{tom 88.8}

fmt.Println(stu.Name) //等价于(*stu).Name 输出tom
fmt.Println(stu.Score) //等价于(*stu).Score 输出88.8
}

  • 如果结构体中的字段也是小写,只能在定义结构体的包中使用,其他包不可访问,同理可以为结构体绑定一个公有方法,用于返回字段
    1)model包下有结构体Student
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package model 

    type student struct{
    //这里结构体定义为小写student,只能在当前包内使用,其他包不可访问
    Name string
    score float64
    //这里字段score定义为小写,只能在当前包内使用,其他包不可访问
    }

    //使用工厂模式
    func NewStudent(name string,score float64) *student{
    return &student{
    Name : name,
    score : score,//该包内可以访问小写字段,其他包不可以
    }
    }

    //GetScore方法绑定结构体,返回字段score
    func (s *student) GetScore() float64{
    return s.score
    }

2)main包下使用工厂模式创建结构体student实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"fmt"
"factory/model"
)

func main(){
//使用工厂模式创建student实例
var stu = model.NewStudent("tom",88.8)//返回值为一个指针

fmt.Println(stu) //输出&{tom 88.8}
fmt.Println(*stu) //输出{tom 88.8}

fmt.Println(stu.Name) //等价于(*stu).Name 输出tom
fmt.Println(stu.GetScore()) //输出88.8
}

面向对象编程三大特性

面向对象编程三大特性:封装、继承、多态

1、封装

1、封装:把抽象出来的字段和对字段的操作封装在一起,数据就被保护在内部,程序的其他包只有通过被授权的操作(方法),才能对字段进行操作

2、封装的优点

  • 隐藏实现的细节
  • 提供方法可以对数据进行验证、保证安全合理

3、如何体现封装

  • 对结构体中的属性进行封装
  • 通过方法、包实现封装

4、封装的实现
参考上一个笔记,工厂模式(将结构体、属性定义为包私有,通过公有方法访问)

2、继承

继承可以解决代码复用,提高扩展性、可维护性
oop1
GO使用在一个结构体中嵌套匿名结构体的方式来实现继承
案例:
1、抽取共有字段和方法,创建一个结构体Student

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Student struct{
Name string
Age int
Score float64
}

//共有方法
func (stu *Student) ShowInfo(){
fmt.Printf("学生名=%v年龄=%v 成绩=%v \n",stu.Name,stu.Age,stu.Score)

}

func (stu *Student) SetScore(score int){
stu.Score = score
}

2、创建结构体Pupil

1
2
3
4
5
6
7
8
9
type Pupil struct{
Student //嵌入Student匿名结构体
}

//Pupil特有方法
func (p *Pupil) testing(){
fmt.Printf("小学生正在考试中")

}

3、创建结构体Graduate

1
2
3
4
5
6
7
8
9
type Graduate struct{
Student //嵌入Student匿名结构体
}

//Graduate特有方法
func (g *Graduate) testing(){
fmt.Printf("大学生正在考试中")

}

4、main创建实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func mian(){
//当我们对结构体嵌入了匿名结构体后,使用方法发生了变化
//创建一个Pupil实例
pupil := &Pupil{}
pupil.Student.Name = "tom"
pupil.Student.Age = 8
pupil.testing()
pupil.Student.SetScore(70)
pupil.Student.ShowInfo()

//创建一个Graduate实例
graduate := &Graduate{}
graduate.Student.Name = "marry"
graduate.Student.Age = 8
graduate.testing()
graduate.Student.SetScore(70)
graduate.Student.ShowInfo()
}

继承细节

1、结构体可以使用嵌套匿名结构体的所有字段和方法,即:首字母大写和小写的字段和方法都可以使用

2、访问匿名结构体字段可以简化
在上述案例中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func mian(){
//当我们对结构体嵌入了匿名结构体后,使用方法发生了变化
//创建一个Pupil实例
pupil := &Pupil{}
pupil.Student.Name = "tom"
pupil.Student.Age = 8
pupil.testing()
pupil.Student.SetScore(70)
pupil.Student.ShowInfo()

//创建Pupil实例,访问Pupil结构体中的匿名结构体Student的字段和方法可以简化为
pupil := &Pupil{}
pupil.Name = "tom"
pupi.Age = 8
pupil.testing()
pupil.SetScore(70)
pupil.ShowInfo()

//pupil.Student.Name = "tom" 等价于pupil.Name = "tom",
// pupil.Name 先去pupil找Name字段,如果没有,再去Pupil结构体中的匿名结构体Student找字段Name
//如果找到则访问,否则报错
}

3、当结构体和匿名结构体有相同的字段或方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体来区分

1
2
3
4
5
6
7
8
9
10
11
12
type A struct{
Name string
age int
}

func (a *A) SayOk(){
fmt.Println("A SayOK",a.Name)
}

func (a *A) hello(){
fmt.Println("A hello",a.Name)
}

1
2
3
4
5
6
7
8
type B struct{
A
Name string
}

func (b *B) SayOk(){
fmt.Println("b SayOK",b.Name)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 func mian(){
var b B

b.Name = "jack"//ok
//去结构体B中找字段Name,找到并赋值jack,则b.Name= "jack"

b.SayOK() //b SayOK jack
//去结构体B中找方法SayOK ,找到并执行fmt.Println("b SayOK",b.Name),这里b.Name= "jack"

b.hello() //A hello ""空串
//去结构体B中找方法hello,没有找到,再去匿名结构体A中找,找到并执行fmt.Println("A hello",a.Name),前面的语句并没有为a.Name赋值,则为默认值空串,a.Name就近等于空串

b.A.Name = "tom"//ok
//去结构体B的匿名结构体A中找字段Name,找到并赋值tom,则b.Name= "tom"

b.SayOK() //b SayOK jack
//去结构体B中找方法SayOK ,找到并执行fmt.Println("b SayOK",b.Name),这里b.Name= "jack"

b.A.SayOK() //A SayOK tom
//去结构体B的匿名结构体A中找方法SayOK ,找到并执行fmt.Println("A hello",a.Name),这里a.Name= "tom"

b.hello() //A hello tom
//去结构体B中找方法hello,没有找到,再去匿名结构体A中找,找到并执行fmt.Println("A hello",a.Name),前面的语句为a.Name赋值tom,则a.Name就近等于tom

}

4、当结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须要明确指定匿名结构体的名字,否则编译报错

5、如果一个struct嵌套了一个有名字的结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段和方法时,就必须带上结构体的名字,不能简写
Go中层级关系明确,如果是组合关系,例:一个结构体A嵌套了一个有名结构体B,B的名字为b,B有字段Name,现创建A的实例a,访问字段Name时,只能a.b.Name,不能a.Name,编译器会报错。看到a.Name时,编译器会去A中找字段Name,没有就立马报错(嵌套匿名结构体时会继续在匿名结构体中找)

6、嵌套匿名结构体后,也可以创建结构体变量时,直接指定各个匿名结构体字段的值

1
2
3
4
5
6
7
8
9
10
pupil := &Pupil{Student{"tom"1578.5}}

//或者
pupil := &Pupil{
Student{
Name:"tom"
Age:15
Score:78.5
}
}

7、在结构体中嵌入匿名结构体时,也可以直接嵌入匿名结构体的指针,这样效率更高

8、在结构体中嵌入匿名结构体时,也可以直接嵌入匿名基本数据类型。
如嵌入匿名int,访问时直接通过 “外层结构体实例名.int = 20”这种形式。但是不能同时侵入两个同样的匿名基本数据类型,因为无法区分,如嵌入两个匿名int,这是不允许的。

多重继承

一个struct嵌套了多个匿名结构体,那么该结构体可以直接访问所有嵌套的匿名结构体的字段和方法,从而实现多重继承
1、当结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须要明确指定匿名结构体的名字,否则编译报错

2、为了代码简洁性,建议尽量不适用多重继承

3、多态

1、基本介绍

1、变量(实例)具有多种形态,在GO中,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现,这时接口变量就呈现不同的形态
案例说明:
1、定义一个接口Usb

1
2
3
4
5
type Usb interface{
//声明两个没有实现的方法
Start()
Stop()
}

2、定义一个结构体Phone

1
2
3
4
5
6
7
8
9
10
11
  type Stu struct{
}

//让Phone实现接口Usb的所有方法,即实现了接口Usb
func (p Phone) Start(){
fmt.Println("手机开始工作。。。")
}

func (p Phone) Stop(){
fmt.Println("手机停止工作。。。")
}

3、再定义一个结构体Camera

1
2
3
4
5
6
7
8
9
10
11
 type Stu Camera{
}

//让Camera实现接口Usb的所有方法,即实现了接口Usb
func (c Camera) Start(){
fmt.Println("相机开始工作。。。")
}

func (c Camera) Stop(){
fmt.Println("相机停止工作。。。")
}

4、再定义一个结构体Computer

1
2
3
4
5
6
7
8
9
type Stu Computer{
}

//编写一个方法Working,接收一个Usb接口类型变量
func (c Computer) Working(usb Usb){
//通过usb接口变量来调用Start和Stop方法
usb.Start()
usb.Stop()
}

5、在main函数中使用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func main(){
//创建结构体Computer变量
computer := Computer{}

//创建结构体Phone变量
phone := Phone{}

//创建结构体Camera变量
camera := Camera{}

//关键点
computer.Working(phone)
//手机开始工作。。。
//手机停止工作。。。

computer.Working(camera)
//相机开始工作。。。
//相机停止工作。。。

//结构体Computer的Working函数接收的是一个Usb接口变量,因为结构体Phone和Camera都实现了这个接口,所以可以传入,并且当传Phone时,可以动态的调用phone.Start()和phone.Stop(),传入camera时同理。Usb接口变量体现出多态。
}

2、接口体现多态的特征

1、多态参数
在前面的Usb接口案例中,Computer的Working函数接收的是一个Usb接口变量,Phone和Camera都实现了这个接口,所以既可以接收Phone变量,又可以接收Camera变量,体现出多态。

2、多态数组
在上述Usb案例中,我们可以定义一个Usb接口数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main(){
//定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
var usbArr [3]Usb
fmt.Println(usbArr)//输出[<nil><nil><nil>]

usbArr[0] = Phone{}
usbArr[1] = Phone{}
usbArr[2] = Camera{}
fmt.Println(usbArr)//输出[<><><>]

//上述的案例中的结构体都没有字段,这里都加上Name字段,然后创建结构体变量
usbArr[0] = Phone{"华为"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"索尼"}
fmt.Println(usbArr)//输出[<华为><小米><索尼>]

//Usb接口数组usbArr,可以存放Phone和Camera的结构体变量,体现出多态数组
}

类型断言

1、基本介绍

1、类型断言,由于接口是一般类型,不知道具体类型。如果要转成具体类型,就需要使用类型断言。

2、案例

1、定义结构体Point

1
2
3
4
type Point struct{
x int
y int
}

2、main函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func main(){
var a interface{} //定义一个空接口类型变量a
var point Point = Point{1,2} //定义Point类型变量point

a = point //ok,
//将Point类型变量point赋给一个空接口类型变量a,
//使空接口类型变量a指向Point类型的变量point
//空接口变量可以接收任何数据类型

var b Point
//b = a不可以,不可以将一个空接口类型变量a赋给其他数据类型
//b = a.(Point)可以,a.(Point)称为类型断言

b,ok = a.(Point)
//判断空接口类型变量a是指向Point类型的变量
//如果是就转成Point类型的变量并赋给b,否则报错

//类型断言检查,不要让断言失败直接panic终止程序
if ok {
fmt.Println(b)//输出<1,2>
}else{
fmt.Println("转换失败")
}

fmt.Println("代码继续执行")//类型断言检查,断言失败不会panic终止程序,这里还会执行到
}

重点

  • 在进行类型断言时,如果类型不匹配,就会报panic。因此进行类型断言时,要确保原来的空接口指向的就是要断言的类型
  • 例:首先“a = point ”使空接口类型变量a指向Point类型的变量point,才可以类型断言“b = a.(Point) ”
3、类型断言最佳案例

assert1

接口

1、基本介绍

1、interface类型可以定义一组方法但是不需要实现

2、interface中不能包含任何变量

2、基本语法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type 接口名 interface{
method1(参数列表)返回值列表
method2(参数列表)返回值列表
...
}

type 自定义类型名 struct{

}

func (s 自定义类型名) method1(参数列表)返回值列表{
//方法实现
}

func (s 自定义类型名) method2(参数列表)返回值列表{
//方法实现
}

...

注意:
1)接口中的所有方法都没有方法体,即接口的方法都是没有实现的方法。体现多态高内聚低耦合

2)Go中的接口,不需要显示实现。只要一个结构体变量(实例)含有接口类型中的所有方法,则称这个结构体变量实现了这个接口(没有类似implement这样的关键字,GO是基于方法实现的多态,而不需显示指出接口名称)。

3)如果由接口a和接口b有完全一样的方法列表,那么有实例c如果实现了接口a,同时也就实现了接口b,即:同时实现两个或两个以上的接口(在其他语言中是做不到的,例如Java,必须显示implement实现a,再显示implement实现b)

4)特别指出,需要实现接口所有的方法

3、注意事项

1、接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型(如struct)的变量(实例)

2、接口中的所有方法都没有方法体,即接口的方法都是没有实现的方法

3、Go中,一个自定义类型需要将某个接口的所有方法都实现,才能称这个自定义类型实现了该接口

4、只要是自定义类型,都可以实现接口,不仅仅是结构体类型。如自定义一个int类型“type integer int”,这个自定义的类型integer也可以实现接口

5、一个自定义类型可以实现多个接口。(只要把多个接口的方法都实现就可以)

6、Go接口中不能有任何变量(常量也不可以)

7、一个接口(如接口A)可以继承多个别的接口(如接口B和C),这时如果要实现接口A,也必须要将接口B和C的方法全部实现,但是接口B和C中不能有完全相同的方法,编译器会报错重复定义,因为无法区分。

8、interface类型默认是一个指针引用类型),如果没有对interface初始化就使用,那么会输出nil

9、空接口interface{},没有任何方法,所以所有类型都默认实现了空接口(即空接口可以接收任何数据类型,可以把任何数据类型变量赋给空接口)。

10、空接口也是一种数据类型
例:可以声明一个空接口类型变量
“vat t interface{}”

该空接口类型变量可以接收任何数据类型的值
“t = student”//接收一个Student类型的变量student
“t = 8.8”//接收一个float类型的值8.8

4、接口使用案例

1、定义一个接口AInteger

1
2
3
4
type AInteger interface{
Test01()
Test02()
}

2、再定义一个接口BInteger

1
2
3
4
type AInteger interface{
Test01()
Test03()
}

3、定义一个结构体Stu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Stu struct{
}

//实现方法Test01
func (stu Stu) Test01(){
}

//实现方法Test02
func (stu Stu) Test02(){
}

//实现方法Test03
func (stu Stu) Test03(){
}

4、在main函数中使用接口

1
2
3
4
5
6
7
8
9
10
11
func main(){
stu := Stu{}//创建一个Stu实例

var a AInteger =stu
//ok,将实例stu赋给接口AInteger的变量a,因为stu实现了接口AInteger中的所有方法( Test01和Test02)

var b BInteger =stu
//ok,将实例stu赋给接口BInteger的变量b,因为stu实现了接口BInteger中的所有方法( Test01和Test03)

fmt.Println("ok",a,b)//ok
}

5、接口与继承比较

1、当B结构体继承了A结构体,那么B结构体就自动的继承了A结构体的字段和方法,并且可以直接使用

2、B结构体需要扩展功能,同时不希望破坏继承关系,可以去实现某个接口,因此,可以认为:实现接口是对继承机制的一种补充

3、接口与继承解决的问题不同

  • 继承的价值:解决代码的复用性可维护性
  • 接口的价值:设计,设计好各种规范(方法),让其他自定义类型去实现这些方法来增强功能

4、接口比继承更加灵活。继承是满足 is - a 的关系,接口只需要满足 like - a 的关系

5、接口在一定程度上实现代码解耦,尤其在GO中更加松散

go:错误处理机制、数组、切片、map

1、go错误处理机制

1、Go引入处理方式:defer、panic、recover,通常三者结合使用
2、这几个异常使用场景简单描述:Go中可以抛出一个panic异常,然后在defer中通过recover捕获这个异常,然后正常处理
示例:

1
2
3
4
5
6
7
8
9
10
11
func test(){
num1 := 10
num2 := 0
res := num1/num2 //会报错,10/0错误,0不可以作为除数
fmt.Println("res=",res)
}

func main(){
test()//test函数报错,下面的代码不会执行
fmt.Println("main下面的代码")
}

异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func test(){
//使用defer+recover结合捕获并处理异常
defer func(){
err :=recover()//recover()是一个内置函数,可以捕获异常
if err != nil{//如果捕获到异常
fmt.Println("err=",err)
//这里可以把错误信息发送给管理员
}
}()//这里调用defer后的匿名函数
num1 := 10
num2 := 0
res := num1/num2 //会报错,10/0错误,0不可以作为除数
fmt.Println("res=",res)
}

func main(){
test()//defer+recover结合捕获并处理异常后,使得下面的代码可以执行
fmt.Println("main下面的代码")
}

自定义错误

1、Go支持自定义错误,使用errors.Newpanic内置函数
1)errors.New(“错误说明”),返回一个error类型的值,表示一个错误
2)panic内置函数,接收一个空接口interface{}类型的值(即任何值)作为参数。可以接收error类型的变量,输出错误信息,并退出程序。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//函数读取配置文件init.conf的信息
//如果文件名传入不正确,返回一个自定义错误
func readConf(name string) (err error){
if name == "init.conf"{//如果捕获到异常
//读取...
return nil
}else {
//返回一个自定义错误
return errors.New("读取文件错误")
}
}

//测试
func test(){
err := readConf(init.conf)
if err != nil{
//如果发生错误,输出错误,并终止程序
panic(err)
}
fmt.Println("test后面的代码")
}

func main(){
test()//test函数如果发生读取文件错误,panic会终止程序,下面的代码不会执行
fmt.Println("main下面的代码")
}

2、数组

1、数组可以存放多个同一类型数据。数组也是一种数据类型,Go中,数组是值类型
2、四种初始化数组方式
如果定义时不赋值,则会被系统赋默认值

  • 1)

    1
    var arr [3]int = [3]int{1,2,3}
  • 2)var arr时不指定类型,类型推导

    1
    var arr = [3]int{1,2,3}
  • 3)用“…”代替数组大小

    1
    var arr = [...]int{1,2,3}
  • 4)指定元素值的下标

    1
    var arr = [3]string{1:"tom",2:"jack",0:"marry"}//顺序可乱序,输出按下标输出
3、数组遍历

1、常规for循环遍历

1
2
3
for i := 0; i < len(arr);i++{
fmt.Printf("arr[%d]=%v\n]",i,arr[i])
}

2、for-range结构遍历

1
2
3
for index,value := range arr{
fmt.Printf("i=%v,v=%v\n",index,value)
}

4、数组使用细节

1、var arr []int,这时arr是一个slice切片
2、Go数组属于值类型,默认情况下是值传递,进行值拷贝,数组间不会互相影响
3、如想在其他函数中修改原来的数组,可以使用引用传递(指针方式,传数组地址)
4、长度是数组类型的一部分,在传递函数参数时,需要考虑数组长度

1
2
3
4
5
6
7
8
9
func modify(arr []int){
arr[0]=100
fmt.Println("modify的arr",arr)
}

func main(){
var arr =[...]int{1,2,3}
modify(arr)//这里编译错误,因为认为main函数中的[3]int与modify函数中的形参[]int不是同一类型
}

3、切片

1、切片是数组的一个引用,因此数组时引用类型,传递方式是引用传递
2、切片的长度可以变化,因此切片是一个动态数组
3、切片的使用和数组类似,遍历、访问切片的元素和求切片长度len(slice)都一样
3、切片定义的基本语法

1
var slice []int//与数组不同的是,【】中不需要填入大小或“...”

4、示例

1
2
3
4
5
6
7
8
9
10
//定义一个数组
var intArr [5]int = [...]int{1,22,33,66,99}

// intArr[1:3]表示切片slice从intArr这个数组的下标为1的元素开始引用,到下标为3的元素,但不包含标为3的元素
slice := intArr[1:3]
fmt.Println("slice的元素:",slice) //[22,33]
fmt.Println("slice的元素个数:"len(slice)) //2
fmt.Println("slice的容量:"cap(slice)) //4

//切片的容量可以动态变化

1、切片在内存中的布局

slice1

由上图可以看出
1、slice是一个引用类型
2、slice从底层来说是一个结构体是struct,由三部分构成

1
2
3
4
5
type slice struct{
ptr *[2]intArr //指向切片第一个元素的地址
len int //长度
cap int //容量
}

由于slice是引用类型,所以通过slice去修改引用到的intArr数组中的值,intArr本身的值也会发生改变

2、切片使用的三种方式

1、定义一个切片,然后让切片去引用一个已经创建好的数组,如上所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义一个数组
var intArr [5]int = [...]int{1,22,33,66,99}

// intArr[1:3]表示切片slice从intArr这个数组的下标为1的元素开始引用,到下标为3的元素,但不包含标为3的元素
slice := intArr[1:3]
fmt.Println("slice的元素:",slice) //[22,33]
fmt.Println("slice的元素个数:"len(slice)) //2
fmt.Println("slice的容量:"cap(slice)) //4

//切片的容量可以动态变化

//几种引用数组元素写法
slice1 := intArr[:3] //等价于intArr[0:3] 1,22,33
slice2 := intArr[1:] //等价于intArr[1:len(intArr)] 22,33,66,99
slice1 := intArr[:] //等价于intArr[0:len(intArr)] 1,22,33,66,99

2、通过make来创建切片
基本语法

1
2
var slice []type = make([]type,len,cap)
//cap可选参数,cap>=len

使用案例

1
2
3
4
5
6
7
var slice []float64
//对于切片,必须make后使用
fmt.Println("slice=",slice)//输出[]

//make后
slice = make([]float64,5,10)
fmt.Println("slice=",slice)//输出[0,0,0,0,0],默认值为0

3、定义切片时直接指定具体数组

1
2
var slice []int = []int{1,3,5}
fmt.Println(slice)

注意:

  • 方式1创建切片的方式是直接引用一个事先定义好的数组,程序员对这个数组可见
  • 方式2通过make方式创建切片,make也会创建一个数组,由切片在底层进行维护,程序员不可见
  • 切片定义完之后,还不能直接使用,这时是一个空切片【】,需要引用到一个数组或者make一个空间来使用
  • 切片可以再切片
3、切片的遍历

1、for循环

1
2
3
4
5
6
var arr [5]int = [...]int{10,20,30,40,50}
slice := arr[1:4] //20,20,30

for i := 0;i<len(slice);i++{
fmt.Printf("slice[%v]=%v ",i,slice[i])
}

2、for-range

1
2
3
4
5
6
var arr [5]int = [...]int{10,20,30,40,50}
slice := arr[1:4] //20,20,30

for i,v := range slice{
fmt.Printf("i=%v v=%v\n",i,v)
}

4、切片的使用注意事项

1、切片定义完之后,还不能直接使用,这时是一个空切片【】,需要引用到一个数组或者make一个空间来使用

2、切片可以再切片

3、用append内置函数,可以对切片动态追加

1
2
3
4
5
6
7
8
var slice []int = []int{100,200,300}
//通过append直接给切片slice追加具体的值,数据类型需匹配
slice = append(slice,400)
//append操作后,会生成一个新数组,需要将append后的值赋给slice,保证slice引用到append操作后的新数组
slice = append(slice,500)
slice = append(slice,600,700,800)//一次追加多个
slice = append(slice,slice...)//直接追加一个切片
fmt.Println(slice)

  • append操作的本质就是对数组扩容
  • go底层会创建一个新的数组newArr,将slice原来包含的元素拷贝到新数组newArr中,slice再重新引用数组newArr
  • 数组newArr在底层维护,程序员不可见

4、用copy内置函数,可以对切片拷贝

1
2
3
4
5
6
7
var a []int = []int{1,2,3,4,5}

var slice = make([]int,10)
fmt.Println(slice)//输出[0,0,0,0,0,0,0,0,0,0]

copy(slice,a)//将切片a的值拷贝到slice中
fmt.Println(slice)//输出【1,2,3,4,5,0,0,0,0,0】

  • copy操作需要两个参数都是切片,只能从切片拷贝到切片
  • 上面代码中,切片a与slice的数据空间是独立的,互不影响
  • 将切片a拷贝到slice,slice的长度小于a也是正确的
  • 切片是引用类型,在传递时遵守引用传递机制
5、slice与string

1、string底层是一个byte数组,因此string也可以进行切片处理操作

1
2
3
4
str := "hello world"
//使用切片获取到world
slice := str[6:]
fmt.Println(slice)//输出world

2、string在内存中的形式,以串“abcd”为例
slice2

string底层由两部分组成,指向一个字节数组的指针[4]byte和长度len
字节数组中真正存放串的内容

3、string是不可变的,即不能通过str[0]=’z’的方式来修改字符(编译会报错)

4、如果需要修改字符串,可以先将string->[]byte(或者 []rune)->修改->重写转成string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    str := "hello world"
//把‘w’改成h
//首先转成byte切片,可以处理英文和数字,中文用[]rune
//因为[]byte按字节来处理,而一个汉字占3个字节,因此会出现乱码,[]rune按字符处理,兼容汉字
slice := []byte(str)
slice[6]='h'
str = string(slice)
fmt.Println(slice)//输出hello horld
```

### 4、映射map
1、map是key-value数据结构,又称为字段或者关联数组

2、声明基本语法
```go
var myMap map[keyType]valueType
```

* key可以是很多种类型,如:bool,int系列、string、指针、channel,还可以是只包含前面几个类型的接口、结构体、数组,**通常为int、string**
* **key不可以是slice、map、function**,因为这几个不能用“==”判断(key通常需要判断key存不存在)

* value的类型通常是string、map、struct

* key不可重复,value可重复,**key**是**无序的**

* **声明不会分配内存**,初始化需要make分配内存后才能赋值和使用
```go
var a map[string]string //声明,还未分配内存

a["no1"]="宋江" //这里会panic恐慌,给一个空map赋值,会报错
fmt.Println(a)

使用map前需要先make,给map分配空间,和数组不一样,数组声明后会默认初始化为零值

1
2
3
4
5
6
7
8
var a map[string]string //声明,还未分配内存

//使用map前需要先make,给map分配空间,和数组不一样,数组声明后会默认初始化为零值
a=make(map[string]string,10)//分配10个空间,不写默认为1
a["no1"]="宋江"
a["no2"]="吴用"

fmt.Println(a) //输出 map 【no1:宋江 no2:吴用】

1、map的使用方式

1、声明->make->赋值
1
2
3
4
5
6
7
8
var a map[string]string //声明,还未分配内存,这时map==nil

//使用map前需要先make,给map分配空间,和数组不一样,数组声明后会默认初始化为零值
a=make(map[string]string,10)//分配10个空间,不写默认为1
a["no1"]="宋江"
a["no2"]="吴用"

fmt.Println(a) //输出 map 【no1:宋江 no2:吴用】
2、声明同时make->赋值
1
2
3
4
5
6
city := make(map[string]string)
city["no1"]="北京"
city["no2"]="上海"
city["no3"]="武汉"

fmt.Println(city) //输出 map 【no1:北京 no2:上海 no3:武汉】
3、声明时直接赋值
1
2
3
4
5
6
7
city := map[string]string{
“no1”:"北京"
“no2”:"上海"
“no3”:"武汉"
}

fmt.Println(city) //输出 map 【no1:北京 no2:上海 no3:武汉】

2、map的增删改查(crud)操作

1、增加和更新
1
2
3
map["key"] = value
//1、如果key之前不存在,就是增加操作
//2、如果key之前存在,就是修改操作,新值覆盖旧值
2、删除
1
delete(map,"key")

1、delete是一个内置函数,如果key存在就删除该key-value,如果不存在,不操作,也不会报错
2、如果map为nil也不操作,不报错
3、如果我们要删除map所有的key,只能遍历逐个删除,不能一次全删除。或者map = make(map[keyType]valueType),make一个新的,让原来的成为垃圾,被GC回收

3、查找
1
2
3
4
5
6
7
8
9
10
11
12
13
city := map[string]string{
“no1”:"北京"
“no2”:"上海"
“no3”:"武汉"
}

//查找
val,ok := city["no1"]
if ok {
fmt.Println("有key:no1,值为:%v",val)
}else {
fmt.Println("没有key:no1")
}

3、map的遍历

map遍历只能通过for-range,不能用普通for循环,因为map没有下标,key是无序的

1
2
3
4
5
6
7
8
9
10
city := map[string]string{
“no1”:"北京"
“no2”:"上海"
“no3”:"武汉"
}

//遍历
for k,v := range city {
fmt.Printf("k=%v v=%v \n",k,v)
}

4、map的长度

len(map),统计map中有几对key-value

1
2
3
4
5
6
7
city := map[string]string{
“no1”:"北京"
“no2”:"上海"
“no3”:"武汉"
}

fmt.Println("city 有"len(city),“对key-value”)

5、map切片

切片的数据类型如果是map,则成为map切片,这样map的个数可以动态变化,map切片是一种切片,切片的每个元素都是一个map.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//声明一个map切片
var monsters []map[string]string

//切片make后才能使用
monsters = make([]map[string]string,2)//len=2

if monsters[0] == nil{
//切片的元素是map,map需要先make再使用
monsters[0] = make(map[string]string,2)//map有两个key
monsters[0]["name"] = "牛魔王"
monsters[0]["age"] = “500
}

if monsters[1] == nil{
//切片的元素是map,map需要先make再使用
monsters[1] = make(map[string]string,2)//map有两个key
monsters[1]["name"] = "玉兔精"
monsters[1]["age"] = “400
}

//因为一开始定义切片长度为2,如果还要添加元素,用append函数动态增加
//1、定义一个monster信息
newMonster := map[string]string{
"name" = "新妖怪:红孩儿",
"age" = "200"
}

//2、添加到切片中
monsters = append(monsters,newMonster)

fmt.Println(monsters)

6、map排序

1、GO中没有专门的map排序方法
2、map默认是无序的,也不是按照添加顺序存放,每次遍历的输出结果也不一样
3、对map排序,可以先将key排序,再根据key值遍历输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
map1 := make(map[int]int,10)
map1[10]=130
map1[1]=13
map1[4]=56
map1[8]=90

fmt.Println(map1)//这里每次的输出结果顺序可能不一致

//排序
//1、先将map的key放到切片中
var keys []int
for k,v := range map1{
keys = append(keys,k)
}

//2、对切片排序
sort.Ints(keys)
fmt.Println(keys)//key递增输出

//3、遍历切片,根据key值遍历输出值
for _,k := range keys{
fmt.Printf("map1[%v]=%v \n",k,map1[k])
}

7、map使用细节

1、map是引用类型,遵守引用传递机制。
2、map容量到达后,想再增加元素,会自动扩容,不会panic,slice会panic
3、map的value也经常使用struct类型

go:函数

1、函数

1、函数调用底层机制

有如下示例
下图中的内存空间划分均为逻辑划分

  1. 从main函数入口开始执行程序
    func1
    在内存中会为main函数在栈上开辟一个空间,并为变量n1赋值10

2、调用函数test()
func2
逻辑上内存会为函数test()在栈上另外开辟一个空间(逻辑上),用于存放test()函数中的数据

3、调用函数test(),并传入参数n1
func3
test()函数接受main()函数传过来的参数n1,在test栈区中另开辟一个空间存放传过来的参数,此时,main栈区和test栈区的n1已经没有关系了(引用类型除外),所以test栈区中n1的改变不影响main栈区中的n1

4、test()函数执行n1=n1+1
func4
test栈区中n1的改变不影响main栈区中的n1

5、test()函数执行打印n1语句,终端输出test栈区中的n1
func5

6、test()函数执行完之后,内存回收test栈区,test栈区中数据全部被清除,程序回到main()函数调用test()函数的地方
func6

7、main()函数执行打印n1语句,终端输出main栈区中的n1
func7

8、main()函数执行完之后,内存回收main栈区,main栈区中数据全部被清除,程序结束
func8

9、总结

  • 在调用一个函数时,会为该函数分配一个新的栈空间,编译器会通过自身的处理让这个新的空间和其他的栈区空间区分开来
  • 在每个函数对应的栈中,数据空间是独立的,不会混淆
  • 当一个函数调用(执行)完毕后,程序会销毁这个函数对应的栈空间

10、注意事项

  • Go不支持传统的函数重载
  • Go中,函数本身也是一种数据类型。可以赋值给一个变量,则该变量就是一个函数类型的变量,通过该变量可以对函数调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    func getSum(n1 int,n2 int) int {
    return n1+n2
    }

    func main(){
    a:=getSum //把函数getSum赋给变量a
    fmt.Println("a的类型是%T,getSum的类型是%T\n",a,getSum)

    res := a(10,40)//等价于 res := getSum(10,40)
    fmt.Println("res=",res)
    }
  • 因为函数是一种数据类型,所以在Go中,函数可以作为形参并调用

    1
    2

    \
  • Go支持对函数返回值命名

    1
    2
    3
    4
    5
    6
    7
    8
    func getSumAndSub(n1 int,n2 int) (sum int, sub int) {
    ///返回值命名sum和sub
    sub = n1 - n2
    sum = n1 + n2
    //为sum和sub赋值,顺序无所谓,可与返回值列表顺序不同
    return
    //等价于return sum , sub ,由于已为返回值命名sum和sub,函数体中已为sum和sub赋值,return时可省略不写返回值,再写上反而显得冗余
    }
  • Go支持可变参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
     //支持0到多个参数
    func sum(args... int) sum int {

    }

    //支持1到多个参数
    func sum(n1 int , args... int) sum int {

    }

args是切片slice,通过args[index]可以访问多个值

2、init函数

1、每一个源文件中都可以包含init函数,该函数会在main函数执行之前,被Go运行框架调用,即init函数在main函数之前被调用。该函数通常用作初始化工作

2、如果一个文件同时包含全局变量定义,init函数和main函数,则执行的流程是:全局变量定义->init函数->main函数

3、如果包含main函数的包里import了其他包,执行流程如下
func9

3、匿名函数

1、如果某个函数我们只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用

2、匿名函数的三种使用方式

  • 在定义匿名函数时就直接调用,且只能调用一次
1
2
3
4
5
6
7
8
func main(){
res1 := func(n1 int, n2 int) int{
return n1 + n2
}(10,20)
//在定义匿名函数的同时就调用它

fmt.Println("res1=",res1)
}

通过在紧跟匿名函数定义的后面传入参数调用
func10

  • 将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数
1
2
3
4
5
6
7
8
9
10
11
12
13
func main(){
//将匿名函数func(n1 int, n2 int) int 赋值给变量a
//则a的数据类型就是函数类型,可以通过变量a完成调用
a := func(n1 int, n2 int) int{
return n1 + n2
}

res2 := a(10,20)
res3 := a(90,20)

fmt.Println("res2=",res2)
fmt.Println("res3=",res3)
}
  • 将匿名函数赋给一个全局变量,则这个匿名函数成为一个全局匿名函数,可以在整个程序有效
1
2
3
4
5
6
7
8
9
10
11
var(
Func1 = func(n1 int, n2 int) int{
return n1 + n2
}
//这时func1即是一个全局匿名函数
)

func main(){
res4 := Func1(4,9)
fmt.Println("res4=",res4)
}

4、闭包

1、闭包:就是一个函数与其相关的引用环境组合的一个整体(实体)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//累加器
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}

func main(){
f := AddUpper()
fmt.Println(f(1)) //输出11
fmt.Println(f(2)) //输出11+2=13
fmt.Println(f(3)) //输出13+3=16
}

对上面代码的说明

  • AddUpper是一个函数,处于外层,返回数据类型时func(int) int
  • 闭包的说明

func11

在上图红框中可以看到,return返回一个匿名函数,处于内层,这个匿名函数引用到外层函数的变量n,因此这个匿名函数和变量n形成一个整体,构成闭包

  • 可以理解成:闭包是一个类,函数是操作,变量n是字段。函数和它使用到的字段n构成闭包

  • 在main函数中,外层函数AddUpper只被调用一次,所以AddUpper函数中的n只被初始化一次

  • 当我们反复调用内层函数f时,因为外层函数中的变量n只初始化一次,因此每次调用进行累计,而不会重新初始化

  • 闭包的关键,是要分析出返回的内层函数它所引用到的外层函数中的变量,因为函数和它所引用到的变量构成闭包

5、函数中的defer

1、为什么需要defer
在函数中,经常需要创建资源(如数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go提供defer(延迟机制)

2、defer使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func sum(n1 int,n2 int) int {
//当执行到defer时,暂时不执行defer后的语句,会将defer后的语句压入独立的栈(这个栈和main函数及sum函数的栈不一样,这里称为defer栈)中

//当函数执行完毕后,再从defer栈中,按先入后出顺序出栈执行
defer fmt.Println("n1=",n1)//3 n1=10
defer fmt.Println("n2=",n2)//2 n2=20

res := n1+n2
fmt.Println("n1+n2=",res)//1 n1+n2=30
return res
}

func main(){
res := sum(10,20)
fmt.Println("res=",res)//4 res=30
}

输出顺序:

1
2
3
4
n1+n2=30
n2=20
n1=10
res=30

3、当执行到defer时,暂时不执行defer后的语句,会将defer后的语句压入独立的栈中,然后执行函数的下一条语句

4、当函数执行完毕后,再从defer栈中,按先入后出顺序出栈执行

5、将defer后的语句放入栈时,也会将相关的值拷贝同时入栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func sum(n1 int,n2 int) int {
defer fmt.Println("n1=",n1)//defer 3 n1=10
defer fmt.Println("n2=",n2)//defer 2 n2=20

//增加两条语句
n1++ //n1=11
n2++ //n2=21
//这里n1、n2自增后,并不影响defer栈中的值,因为defer之后的语句入栈时,同时将值拷贝入栈,所以defer中的n1、n2值仍是入栈时候的值

res := n1+n2
fmt.Println("n1+n2=",res)//1 n1+n2=32
return res
}

func main(){
res := sum(10,20)
fmt.Println("res=",res)//4 res=32
}

输出顺序:

1
2
3
4
n1+n2=32
n2=20
n1=10
res=32

6、函数参数传递的方式

1、两种传递方式

  • 值传递
  • 引用传递

不管是值传递还是引用传递,传递给函数的都是变量的副本。不同的是:值传递传递的是值的拷贝引用传递传递的是地址的拷贝

一般来说,地址拷贝效率高,因为数据量小,而值拷贝的效率由传递的数据大小决定,数据越大,效率越低。

2、值类型和引用类型

  • 值类型:基本数据类型(int系列、float系列、bool、string)、数组、结构体
  • 引用类型:指针、slice切片、map、管道channel、interface

7、字符串中常用系统函数

1、统计字符串的长度,按字节len(str)
是一个内置(内建)函数,不属于任何包,可直接使用

2、字符串遍历,同时处理 有中文的问题:r := []rune(str)
遍历字符串时,可先将string类型的字符串str转成rune的切片:[]rune(str),再进行遍历,因为string类型按字节遍历,[]rune按字符遍历,一个中文占三个字节。(for循环按字节遍历,for … range 按字符遍历)

3、字符串转整数:n,err := strconv.Atoi(“12”)

4、整数转字符串:str = strconv.Itoa(12345)

5、字符串转[]byte:var bytes = []byte(“hello go”)

6、[]byte转字符串:str = string([]byte{97,98,99})

7、十进制转二进制、八进制、十六进制

  • 十进制转二进制:

str = strconv.FormatInt(123,2)

  • 十进制转八进制:

str = strconv.FormatInt(123,8)

  • 十进制转十六进制:

str = strconv.FormatInt(123,16)

8、查找子串是否在指定的串中:strings.Contains(“seafood”,”foo”)//true

9、统计一个字符串中有几个指定的子串:strings.Count(“ceheese”,”e”)//4

10、不区分大小写的字符串比较(用“==”比较区分大小写):strings.EqualFold(“abc”,”Abc”)//true

11、返回子串在字符串中第一次出现的index值,如果没有返回-1:strings.Index(“NLT_abc”,”abc”)//4 下标从0开始索引

12、返回子串在字符串中最后一次出现的index值,如果没有返回-1:strings.LastIndex(“go go hello”,”go”)//3 下标从0开始索引

13、将指定的子串替换成另外一个子串:strings.Replace(“go go hello”,”go”,”go语言”,n)
n可以指定你希望替换几个,n=-1表示全部替换

14、按照指定的某个字符为分割标识,将一个字符串拆分成字符串数组:strings.Split(“hello,world,ok”,”,”)

15、将字符串的字母进行大小写转换:

大写转小写

1
strings.ToLower("Go")//go

小写转大写

1
strings.ToUpper("Go")//GO

16、将字符串的左右两边空格去掉:strings.TrimSpace(“ go hello “)

17、将字符串左右两边指定的字符去掉:strings.Trim(“! hello!”,” !”)
//将【hello】左右两边的!和空格去掉

18、将字符串左边指定的字符去掉:strings.TrimLeft(“! hello!”,” !”)
//将【hello】左边的!和空格去掉

19、将字符串右边指定的字符去掉:strings.TrimRight(“! hello!”,” !”)
//将【hello】右边的!和空格去掉

20、判断字符串是否以指定的字符串开头:

1
strings.HasPrefix("ftp://192.168.10.1","ftp")//true

21、判断字符串是否以指定的字符串结束:strings.HasSuffix(“go_var.jpg”,”jpg”)//true

8、日期和时间相关函数

使用时间和日期函数需要引入time包
1、time.Time类型,用于表示时间

2、获取当前时间
now := time.Now()//now类型是time.Time

1
2
3
4
5
6
7
8
通过变量now获取年月日、时分秒
now.Year()
now.Month()//英文月份
int(now.Month())//int强转为数字月份
now.Day()
now.Hour()
now.Minute()
now.Second()

3、格式化日期

1)方式1使用Printf或者Sprintf
1
2
now := time.Now()
fmt.Printf("当前年月日时分秒 %02d-%02d-%02d %02d-%02d-%02d \n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second())

或者Sprintf,Sprintf会返回一个字符串

1
2
3
4
now := time.Now()
dateStr := fmt.Sprintf("当前年月日时分秒 %02d-%02d-%02d %02d-%02d-%02d \n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second())
//fmt.Sprintf返回一个时间日期字符串赋给变量dateStr,可对变量dateStr进行操作(输出或保存等)
fmt.Printf("dateStr=%v\n",dateStr)

2)方式2使用Format
1
2
3
4
5
6
7
8
now := time.Now()
fmt.printf(now.Format("2006/01/02 15:04:05"))//年月日时分秒
fmt.printf(now.Format("2006/01/02))//年月日
fmt.printf(now.Format("15:04:05"))//时分秒
fmt.printf(now.Format("2006"))//年
fmt.printf(now.Format("01"))//月
fmt.printf(now.Format("02"))//日
fmt.printf(now.Format("2006-01-02 15:04:05"))

“2006/01/02 15:04:05”这一串数字是固定的,不能更改,但格式和组合可以改(传说这串数字是Go开发者脑海中萌生GoLang的时刻)。

4、时间的常量

1
2
3
4
5
6
7
8
const(
Nanosecond Duration = 1 //纳秒
Microsecond = 1000 * Nanosecond //微秒
Millisecond = 1000 * Microsecond //毫秒
Second = 1000 * Millsecond //秒
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)

常量的作用:在程序中可用于获取指定时间单位的时间,比如想得到100毫秒 100 * time.Millisecond

可用于休眠

1
2
3
4
time.Sleep(time.Second)//休眠1秒
time.Sleep(time.Millisecond * 100)//休眠0.1秒
//只能传入整数,不能传小数
//休眠0.1秒不能这么写time.Sleep(time.Second * 0.1)

5、时间戳

  • unix时间戳
    返回1970年1月1日0时0分0秒 到当前时间的秒数

  • unixnano时间戳(unix纳秒时间戳)
    返回1970年1月1日0时0分0秒 到当前时间的纳秒数

可以用于获取随机的数字

1
2
3
4
//为了每次生成的随机数不一样,需要设定一个种子seed
rand.Seed(time.Now().UnixNano())
num := rand.Intn(100)//0<=n<100
//如果不设置种子,会有一个默认值,但每次随机出来的数都一样

9、内置(buildin)函数

1、len():求长度,比如string、array、slice、map、channel

2、func new

1
func new(Type) *Type

new分配分存,第一个实参为类型而非值,new函数返回值为指向该类型新分配零值指针,主要为值类型分配内存
例:

1
2
3
4
5
6
7
8
9
num1 := 100
fmt.Printf("num1的类型%T,num1的值%v,num1的地址%v",num1,num1,&num1)
//num1的类型int,num1的值100,num1的地址0xcXXXXXXXX

num2 := new(int)
fmt.Printf("num2的类型%T,num2的值%v,num2的地址%v,num2指针指向的值",num2,num2,&num2,*num2)
//num2的类型*int,
//num2的值0xcXXXXXXXX,num2的地址0xcXXXXXXXX,num2指针指向的值0,(这两个地址在程序运行时,系统动态分配)
//num2的值是一个地址,该地址指向int类型的零值0

func12

3、make:分配内存,主要用来分配引用类型

go:基本数据类型、运算符

go特点

1、go=c+python,既有静态语言程序运行的速度以及安全和性能,又能达到动态语言(如脚本语言)的开发维护的高效率。
2、go语言的一个文件必须要归属一个包,不能单独存在
3、垃圾回收机制、内存自动回收
4、天然并发(超级重要)

  • 从语言层面支持并发,实现简单
  • goroutine:轻量级线程,可实现大并发处理,高效利用多核
  • 基于CPS并发模型(Communicating Sequential Processes)实现

5、管道通信机制,形成go语言特有的管道channel。通过管道channel可以实现不同的goroute之间的通信

6、函数返回多个值

7、新的创新,如切片slice,延时执行defer等

go运行

如有一个hello.go文件,运行它的两种方法
1、go bulid hello.go先编译这个文件,生成hello.exe二进制可执行文件,再直接切到该目录下输入hello.exe命令即可运行。
2、go run hello.go,编译运行一起执行,直接输出结果

区别:
1、先编译后再运行.exe文件,运行速度很快,go run命令需要编译+运行速度明显慢
2、编译后生成的.exe文件,可以放在没有go开发环境的机器上运行起来,因为hello.exe文件比hello.go文件大,因为编译后的.exe文件动态引入了运行所需要的很多库,所以可以跑起来,但go run命令不行,因为需要开发环境对.go文件进行编译。

变量

变量的几种数据类型
var1

在GO中,数据类型都有一个默认值(零值),基本数据类型的零值如下

  • 整型:0
  • 浮点型:0
  • 字符串:“”空串
  • bool类型:false

格式化%v表示按照变量的值输出

1、整数类型

1、int,uint表示的位数与系统有关,32位系统表示4个字节,64位表示8个字节

2、rune,有符号,字节数等价int32,但表示一个Unicode码,用于处理带中文的字符串。

3、byte,无符号,与uint8等价,用来存储单个字符。
var2

4、golang整型默认声明为int类型

5、查看某个变量的占用字节大小和数据类型
fmt.Printf(“n的数据类型是 %T n占用的字节数是 %d”,n,unsafe.Sizeof(n))

6、在保证程序的正确运行下,尽量使用占用空间小的数据类型,遵守保小不保大的原则,若不知道该变量之后表示的值的大小,可以稍微选用大的。

2、浮点类型

1、浮点类型有固定的范围和字段长度,不受具体操作系统的影响

2、golang浮点类型默认声明为float64

3、开发中,推荐使用float64,因为比float32更精确

3、字符类型

1、Golang中没有专门的字符类型,存储单个字符(如ASCII码所表示的字符)使用byte来保存
注:

  • 直接输出byte变量的值时,实际上是输出了对应字符的ASCII码值,如要输出对应字符,用格式化%c输出
  • 输出int变量表示字符的UTF-8编码的码值时用格式化%d输出

2、Go的字符串是由单个字节连接起来的,与传统字符串由字符连接不同。

3、字符常量是用单引号‘’括起来的单个字符。例var c1 byte=’a’ var c2 int=’中’

4、Go的字符使用UTF-8编码,英文字母一个字节表示,汉字三个字节表示

5、Go中,字符的本质是一个整数,直接输出时是该字符对应的UTF-8编码的码值

6、可以给某个变量赋值一个数字,按格式化输出时%c会输出该数字对应的Unicode字符

7、字符类型可以进行运算,因为本质就是一个整数,都对应有Unicode码值

4、布尔类型

1、Go中bool类型只允许取值true和false

2、bool类型占1个字节

5、字符串类型

1、Go的字符串是由单个字节连接起来的,与传统字符串由字符连接不同。

2、字符串一旦赋值,就不能修改了,即Go中字符串是不可变的

3、字符串两种表示方式

  • 双引号“”,会识别转义字符
  • 反引号,以字符串的原生形式输出,包括换行和转义字符,可以防止攻击,输出源代码等效果。

4、字符串连接使用“+”号,多行连接时需要把“+”号放在上一行末尾

6、基本数据类型转换

1、Go与Java/C不同,Go在不同类型的变量之间赋值时需要显示转换,即不能自动转换

2、数据转换时,被转换的是变量存储的数据(值),变量本身的数据类型不会改变
例:

1
2
3
4
5
int i int32=100

var n int64=int64(i)

//i本身不会改变

3、高精度向低精度转换,低精度向高精度转换都可以,只是高精度向低精度转换时会溢出,和我们预想结果不一致,但编译器不会报错

6、基本数据类型和字符串类型相互转换

1)基本数据类型转string
  1. fmt.Sprintf(“%参数”,表达式)
    1
    2
    3
    func Sprintf(format string,a ...interface{}) string

    //Sprintf根据format参数生成格式化字符串并返回

参数需要和表达式的数据类型相匹配

2.使用strconv包的函数

2)string转基本数据类型
  1. 使用strconv包的函数
3)注意事项

1、string转基本数据类型时,要确保string类型能够转成有效的数据。比如可以把”123“转成一个整数,但不能把”hello“转成一个整数,如果这样做,go不会报错,而是直接转成默认值。

7、指针

1、值类型包括:基本数据类型、数组和结构体struct

2、引用类型包括:指针、切片slice、map、管道channel、interface

3、值类型和引用类型
var3

运算符

1、算术运算符

1、Go中自增自减只能作为一个独立的语句使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
b:=a++//错误
b:=a--//错误

var c int = 1
var d int
d=c++//错误,只能独立使用

var i int = 10
if i++ > 10{//错误,只能独立使用
}

var e int = 1
e++
var f int = e//正确

2、Go的自增自减只有后加加a++和后减减a–,去除了很多容易混淆的写法

2、位运算符

1、原码、反码、补码
op1

1
2
3
4
5
6
7
8
9
10
11
12
-2^2=?

-2 => 原码 1000 0010 => 反码(符号位不变,其他位取反) 1111 1101 =>补码(反码+11111 1110

2 =>为正数,原码、反码、补码一样 =>0000 0010

//计算机运算都是用补码运算
-2^2=1111 1110 ^ 0000 0010 = 1111 1100//结果是补码,需转换为原码

补码 1111 1100 =>反码(补码-11111 1011 =>原码(符号位不变,其他位取反)1000 0100 => 十进制 -4

则:-2^2=-4

2、Go中有两个移位运算符

  • 右移运算符>>:低位溢出,符号位不变,并用符号位补溢出的高位
  • 左移运算符<<:符号位不变,低位补0