Go(上):go基础

文章目录


go语言的发展历史以及与其他语言的相比的特长就不详细说了,大概说下就是,go是很新的,站在巨人肩膀上设计的,原生的并发编程性,垃圾回收处理功能更好,开发与编译运行的效率都极高

部署go环境

这里不不错windows的,用linux环境,linux下开发一切明了,比windows的点点点方便多(只是个人看法,用惯linux,觉得windows是用不友好),mac下的跟linux相差无几

golang的中国官网
在这里插入图片描述
在这里插入图片描述
我这里用最新的,如果你怕不稳定可以挑上一个发行版本的最大版本(实际上编程语言而已,只是工具,不是深入底层的设计,几乎可以不考虑它存在的小bug)
在这里插入图片描述

[root@m1 ~]# tar -x -f go1.18.1.linux-amd64.tar.gz -C /usr/local
[root@m1 ~]# cd /usr/local
[root@m1 local]# ls
bin  etc  games  go  include  lib  lib64  libexec  sbin  share  src
[root@m1 local]# mkdir ~/go
[root@m1 local]# vim ~/.bash_profile
加入下面这些

export GOROOT="/usr/local/go"   #go的安装目录,纯路径用字符串
export GOPATH=$HOME/go   #go的项目开发目录,存放代码的地方
export GOBIN=$GOPATH/bin   #生成的bin目录存放到这里
export PATH=$PATH:/usr/local/go/bin   #注意假的是目录,PATH的内容是目录,这里就是为了将go的bin文件加入PATH
export PATH=$PATH:$GOBIN   #这样你生成的go的执行文件也可以在当前用户任意路径下用命令名运行(前提你给了可执行权限)

source ~/.bash_profile

go的命令式源码虽然说可以在任意地方用go run运行,但始终是单一点,对于项目开发,建议将代码都放在GOPATH目录下,该目录下有src,这个目录是开发者创建的,里面的每一个子目录就是一个包,包内是go的源码文件(命令式源码文件)。GOPATH下还有自动生成的pkg和bin目录,pkg存放编译后生成的目标文件,准确点说是包的目标文件,包下游多分源码文件,一块编译。目标文件不能运行,目标文件进一步链接库操作生成的可执行文件可以运行,存放在bin下

/etc/profile:全局配置,设置环境变量和启动程序,对每个用户都有效
$HOME/.bash_profile:与上面差不多,主要是对象不同,这是只针对当前用户的配置
$HOME/.bashrc和/etc/bashrc,跟上面的两个间的关系很相似,主要也是用来设置些环境变量
bashrc和profile都用于配置用户的环境信息
bashrc用于non-loginshell即非交互式shell,profile用于loginshell

[root@m1 local]# go version
go version go1.18.1 linux/amd64
[root@m1 local]# go env

架构一般是amd64
这里是go1.18.1,go版本更新还是很快的,扯面反映出发展势头

编写第一个HelloWord

[root@m1 ~]# cd go/
[root@m1 go]# mkdir src
[root@m1 go]#
[root@m1 go]# ls
src
[root@m1 go]# cd src
[root@m1 src]# mkdir hello
[root@m1 src]# cd hello/
[root@m1 hello]#
[root@m1 hello]# ls
helloworld.go

[root@m1 hello]# vim helloworld.go
package main

import "fmt"

func main(){
    
    
        fmt.Println("HelloWorld")
}

go run方式运行go代码(命令式源码),想进入go文件所在的目录,然后go run 文件名即可
[root@m1 hello]# go run helloworld.go
HelloWorld


go build方式运行go代码
编译(默认包含链接步骤),直接一步到位得到可运行文件
[root@m1 hello]# go build helloworld.go
[root@m1 hello]# ls
helloworld  helloworld.go
[root@m1 hello]# go build helloworld.go
[root@m1 hello]# ls -l
总用量 1720
-rwxr-xr-x 1 root root 1754257 418 12:51 helloworld
-rw-r--r-- 1 root root      69 412 2019 helloworld.go
[root@m1 hello]# ./helloworld
HelloWorld
[root@m1 hello]#

可以用最简单的方式将go应用部署到linux上,就是程序运行成进程
[root@m1 hello]# nohup ./helloworld &
[1] 11235
[root@m1 hello]# nohup: 忽略输入并把输出追加到"nohup.out"

这里默认是输出到nohup.out,默认当前目录下,可以重定向文件,如nohup command > file 2>&1 &
最好一块处理了标准输出和标准错误输出,将标准错误输出打到标准输出中去
nohup是让命令忽而略中断等命令,&是让命令后台运行,两者一般搭配使用,用户退出终端(账号退出)发送终端信号一般都不会停止运行,但实际来看,命令是否能脱离终端后继续,也要看他是不是守护进程(长期运行进程)
可以ps aux | grep helloworld查看该进程,可以通过kill -9 方式关闭进程
go install方式运行,可以到hello这个源码目录下运行go install,或者在任意目录运行go install hello文件绝对路径
运行go install,新版本有新要求,先运行这个配置环境变量go env -w GO111MODULE=auto

总结三种运行方式go run,go build,go install,做大项目用go install更方便

go命令运行原理,常用命令和参数

同一个的源码包目录下,最好不要放多个命令源码文件,比如上面的helloworld就是个命令源码文件

同一个源码目录下有多个源码文件使用go build和go install会错误

源码目录又可以称为包,go中除了命令源码文件还有库源码文件,就是你导入的包下的命令源码文件。
另外还有测试源码文件,不用过多说这些,后面用到自然明白,主要的就是命令源码文件

go命令:

[root@m1 bin]# go --help
Go is a tool for managing Go source code.

Usage:

        go <command> [arguments]

The commands are:

        bug         start a bug report
        build       compile packages and dependencies
        clean       remove object files and cached files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         update packages to use new APIs
        fmt         gofmt (reformat) package sources
        generate    generate Go files by processing source
        get         add dependencies to current module and install them
        install     compile and install packages and dependencies
        list        list packages or modules
        mod         module maintenance
        work        workspace maintenance
        run         compile and run Go program
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

        buildconstraint build constraints
        buildmode       build modes
        c               calling between Go and C
        cache           build and test caching
        environment     environment variables
        filetype        file types
        go.mod          the go.mod file
        gopath          GOPATH environment variable
        gopath-get      legacy GOPATH go get
        goproxy         module proxy protocol
        importpath      import path syntax
        modules         modules, module versions, and more
        module-get      module-aware go get
        module-auth     module authentication using go.sum
        packages        package lists and patterns
        private         configuration for downloading non-public code
        testflag        testing flags
        testfunc        testing functions
        vcs             controlling version control with GOVCS

Use "go help <topic>" for more information about that 

常用的build get install run

这些命令常用的参数
-a:强制重新编译所有涉及go语言的代码包,包括标准库的包
-n:使命令仅仅打印执行过程用到的所有命令而不是真正去执行
-race:并发方面的手段,用于检测并报告指定go语言程序中存在的数据竞争问题
-v:打印命令执行过程中涉及的包,包括这些包的依赖包
-x:打印命令执行过程中所有用到的命令,并同时执行
-work:打印命令执行时生成的和使用的临时工作目录名字,这个临时目录下的文件可能有用

go run

go run运行的是一个命令源码文件以及多个库源码文件,运行时会产生一个.a的归档文件(pkg)和可执行文件

注意看签名创建的boo1和后面创建的exec文件夹,和生成_pkg_.a归档文件和执行文件

[root@m1 hello]# go run -n helloworld.go

#
# command-line-arguments
#

mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg << 'EOF' # internal
# import config
packagefile fmt=/usr/local/go/pkg/linux_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/linux_amd64/runtime.a
EOF
cd /root/go/src/hello
/usr/local/go/pkg/tool/linux_amd64/compile -o $WORK/b001/_pkg_.a -trimpath "$WORK/b001=>" -p main -complete -buildid jkn                      gnfku6pdlWgr4LbpZ/jkngnfku6pdlWgr4LbpZ -dwarf=false -goversion go1.18.1 -c=4 -D _/root/go/src/hello -importcfg $WORK/b00                      1/importcfg -pack ./helloworld.go
/usr/local/go/pkg/tool/linux_amd64/buildid -w $WORK/b001/_pkg_.a # internal
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=$WORK/b001/_pkg_.a
packagefile fmt=/usr/local/go/pkg/linux_amd64/fmt.a
packagefile runtime=/usr/local/go/pkg/linux_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/linux_amd64/errors.a
packagefile internal/fmtsort=/usr/local/go/pkg/linux_amd64/internal/fmtsort.a
packagefile io=/usr/local/go/pkg/linux_amd64/io.a
packagefile math=/usr/local/go/pkg/linux_amd64/math.a
packagefile os=/usr/local/go/pkg/linux_amd64/os.a
packagefile reflect=/usr/local/go/pkg/linux_amd64/reflect.a
packagefile strconv=/usr/local/go/pkg/linux_amd64/strconv.a
packagefile sync=/usr/local/go/pkg/linux_amd64/sync.a
packagefile unicode/utf8=/usr/local/go/pkg/linux_amd64/unicode/utf8.a
packagefile internal/abi=/usr/local/go/pkg/linux_amd64/internal/abi.a
packagefile internal/bytealg=/usr/local/go/pkg/linux_amd64/internal/bytealg.a
packagefile internal/cpu=/usr/local/go/pkg/linux_amd64/internal/cpu.a
packagefile internal/goarch=/usr/local/go/pkg/linux_amd64/internal/goarch.a
packagefile internal/goexperiment=/usr/local/go/pkg/linux_amd64/internal/goexperiment.a
packagefile internal/goos=/usr/local/go/pkg/linux_amd64/internal/goos.a
packagefile runtime/internal/atomic=/usr/local/go/pkg/linux_amd64/runtime/internal/atomic.a
packagefile runtime/internal/math=/usr/local/go/pkg/linux_amd64/runtime/internal/math.a
packagefile runtime/internal/sys=/usr/local/go/pkg/linux_amd64/runtime/internal/sys.a
packagefile runtime/internal/syscall=/usr/local/go/pkg/linux_amd64/runtime/internal/syscall.a
packagefile internal/reflectlite=/usr/local/go/pkg/linux_amd64/internal/reflectlite.a
packagefile sort=/usr/local/go/pkg/linux_amd64/sort.a
packagefile math/bits=/usr/local/go/pkg/linux_amd64/math/bits.a
packagefile internal/itoa=/usr/local/go/pkg/linux_amd64/internal/itoa.a
packagefile internal/oserror=/usr/local/go/pkg/linux_amd64/internal/oserror.a
packagefile internal/poll=/usr/local/go/pkg/linux_amd64/internal/poll.a
packagefile internal/syscall/execenv=/usr/local/go/pkg/linux_amd64/internal/syscall/execenv.a
packagefile internal/syscall/unix=/usr/local/go/pkg/linux_amd64/internal/syscall/unix.a
packagefile internal/testlog=/usr/local/go/pkg/linux_amd64/internal/testlog.a
packagefile internal/unsafeheader=/usr/local/go/pkg/linux_amd64/internal/unsafeheader.a
packagefile io/fs=/usr/local/go/pkg/linux_amd64/io/fs.a
packagefile sync/atomic=/usr/local/go/pkg/linux_amd64/sync/atomic.a
packagefile syscall=/usr/local/go/pkg/linux_amd64/syscall.a
packagefile time=/usr/local/go/pkg/linux_amd64/time.a
packagefile unicode=/usr/local/go/pkg/linux_amd64/unicode.a
packagefile internal/race=/usr/local/go/pkg/linux_amd64/internal/race.a
packagefile path=/usr/local/go/pkg/linux_amd64/path.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nbuild\t-compiler=gc\nbuild\tCGO_ENA                      BLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\                      tGOOS=linux\nbuild\tGOAMD64=v1\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/linux_amd64/link -o $WORK/b001/exe/helloworld -importcfg $WORK/b001/importcfg.link -s -w -buildmo                      de=exe -buildid=FRIN0D-HeVSKkQkvJn-z/jkngnfku6pdlWgr4LbpZ/jkngnfku6pdlWgr4LbpZ/FRIN0D-HeVSKkQkvJn-z -extld=gcc $WORK/b00                      1/_pkg_.a
$WORK/b001/exe/helloworld
[root@m1 hello]# ls
helloworld.go

[root@m1 hello]# go run -work helloworld.go
WORK=/tmp/go-build3057025931
HelloWorld
[root@m1 hello]# cd /tmp/go-build3057025931
[root@m1 go-build3057025931]# ls
b001
[root@m1 go-build3057025931]# cd b001/
[root@m1 b001]# ls
exe  importcfg.link
[root@m1 b001]# cd exe/
[root@m1 exe]# ls
helloworld
上面这个go run属于第二次运行,就是说helloworld这个文件没变化,需要导入的包也没有变化,go run不会再次编译,而是直接将内容静态链接出来(就是没有compile只有link)

这个是helloworld第一go build时产生的临时目录机器下面的内容
[root@m1 go-build2726030929]# ls
b001
[root@m1 go-build2726030929]# cd b001/
[root@m1 b001]# ls
exe  importcfg  importcfg.link  _pkg_.a

在这里插入图片描述

go build

1.如果是普通的包,执行go build不会产生任何文件
2.如果是main包,会在当前的目录产生可执行文件,也可以go build -o 路径/可执行文件名(output),自然-o也可以改变编译输出的文件的文件名
此时如果使用go install会将可执行文件挪到$GOBIN目录下
3.如果源码目录下有多个文件,go build会编译所有,你也可以通过go build 文件名来制定文件编译,还记得说过同个源码目录下最好不要有多个命令源码文件
4.go build会忽略以_和.开头的文件
5.如果你的代码开发面向不同的操作系统那么需要注意后缀名,linux不以后缀名来判断文件,但windows就不行了,要严格把控后缀名,把命名后缀名常规化当成一种习惯即可
(使用发现go build要在命令源码文件目录下运行才生成可执行文件)

[root@m1 hello]# ls
helloworld.go
[root@m1 hello]# go build
[root@m1 hello]# ls
hello  helloworld.go
[root@m1 hello]# go install
[root@m1 hello]# ls
helloworld.go
[root@m1 hello]# cd ~/go/bin
[root@m1 bin]# ls
hello


[root@m1 hello]# go build -o h.go helloworld.go
[root@m1 hello]# ls
helloworld.go  h.go
好习惯:尽量不自定义名称,同个源码目录下不要放多个源码目录

go install

一般产生两种文件:
可执行文件:go install带main函数的go文件,可以直接执行(移动到bin)
.a应用包:go install不带main函数的go文件,没有函数入口,只能被调用(移动到pkg)

库源码文件go install会变成.a归档文件,移动到pkg下
命令源码文件go install会被查可执行文件,移动到bin下

go get

从远程仓库比如github,gitlab上下载并安装代码包,默认会把代码包下载到$GOPATH的第一个工作区的src工作目录中并安装

-d:只下载不安装
-f:-u才生效,忽略对已下载的代码包的导入路径检查
-fix:先让下载的代码包执行修正动作,在编译安装
-insecure:允许使用非安全的schema(http)去下载代码,有的远程仓库没有https,不在支持,help查看替代的
-t:同时下载并安装指定代码包中的测试源码文件的依赖的代码包
-u:用来更新已有的代码包及其依赖包
-x:看到执行过程

运行go get:
从代码仓库git clone下库源码文件–>go get–>编译,这里生成.a归档文件–>存放到pkg平台目录下

可以先从github上找到对应的包,复制路径,不要带http,然后go get 路径即可下载安装

go doc

查看包的说明
go doc net/http
(有包的函数变量等内容)

go doc fmt printf
查看fmt包的printf函数

还可以通过浏览器来查看包的说明
go doc -http=:9527
你就可以在浏览器上访问127.0.0.1:9527来查看包的说明(少用,用终端就好)

go run go build go install

安装go的IDE集成开发环境goland

习惯命令行的朋友可能会更喜欢用vim来编辑代码,用终端命令来运行代码,所以这里的ide看个人需求决定是否安装

https://www.jetbrains.com/go/
激活码过期了要更新时,进去goland后记得将网络断开
在这里插入图片描述
在这里插入图片描述
一样是下载.tar.gz下来

解包的细节,对于解包,其实最好现在一个无关紧要的目录如/tmp下先测试得解包先,因为有的包一解开不是文件夹,而是一大堆的文件夹或文件

[root@m1 ~]# ls
anaconda-ks.cfg  go1.18.1.linux-amd64.tar.gz  nohup.out  第一阶段源码.zip  模板  图片  下载  桌面
go               goland-2022.1.tar.gz         one        公共              视频  文档  音乐
[root@m1 ~]# cd /opt
[root@m1 opt]# ls
cni  containerd  GoLand-2022.1  rh
[root@m1 opt]# cd GoLand-2022.1/
[root@m1 GoLand-2022.1]# ls
bin  build.txt  help  Install-Linux-tar.txt  jbr  lib  license  plugins  product-info.json
[root@m1 bin]# ls
brokenPlugins.db  fsnotifier          goland.png  goland.svg  idea.properties  libdbm64.so  remote-dev-server.sh  restart.py
format.sh         goland64.vmoptions  goland.sh   icons       inspect.sh       ltedit.sh    repair
[root@m1 bin]# ./goland.sh
运行它可以打开goland这个ide图形化应用,可以添加个快捷方式到桌面

在这里插入图片描述
收费产品,用这个可以免费激活
如果这个激活地址失效了,可以百度找新的
也可以直接找激活码,还有下面那个codewithme可以不用管。
获取激活码
b站也有获取激活码简要教程

进去之后主要设置GOROOT和GOPATH

idea新建的项目其实就是包,就是在$GOPATH的文件夹
在这里插入图片描述
在这里插入图片描述

vim添加go插件与开发环境优化

vim部署go开发环境

git clone下载慢解决

他有个地方写少了.git,自己加上去
安装vim8参考
最好先删除vim7

GoInstallBinaries下载失败解决方法(其实就是自己手动拉去,注意他这个有@latest,go1.18后go install才支持这种写法)

也可以用这条命令查看要拉取的包
 vi ~/.vim/bundle/vim-go/plugin/go.vim

get或install拉取失败

有时候get或install拉去失败,加上这两个go的环境配置
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

加Fn
F2:代码提示
F4:右侧展示文件结构
Ctrl + ]/o:代码追踪/回退

其实熟练之后,有没这个辅助环境都不影响你的开发,只要部署了go编译环境和最原始的vim就够了

回来补充一条更好的vim-go从部署到使用教学的连接

回来补充一条更好的vim-go从部署到使用教学的连接

参考:
https://aiezu.com/article/vim_golang_ide_vundle_vim_go.html

https://segmentfault.com/a/1190000017270664

https://blog.csdn.net/zhang197093/article/details/78559903
各类插件各类包,要什么插件或者包就装什么

包的功能无非就是一般在$GOPATH/bin下,只要下载下来需要的文件,go install即可

自定义快捷键

解决go: go.mod file not found in current directory or any parent directory; see ‘go help modules‘

各命令解释

基础语法

变量

变量关键字:var

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     变量:variable
  8     概念:一小块内存,用于存储数据,在程序运行过程中数值可以改变
  9
 10     使用:
 11         step1:变量的声明,也叫定义
 12             第一种:var 变量名 数据类型
 13                     变量名 = 赋值
 14
 15                     var 变量名 数据类型 = 赋值
 16
 17             第二种:类型推断,省略数据类型
 18                     var 变量名 = 赋值
 19
 20             第三种:简短声明,省略var
 21                     变量名 := 赋值
 22
 23         step2:变量的访问,赋值和取值
 24                 直接根据变量名访问
 25
 26
 27     go的特性:
 28         静态语言:强类型语言
 29             go,java,c++,c#。。
 30
 31         动态语言:弱类型语言
 32             JavaScript,php,python,ruby。。
 33      */
 34      //第一种:定义变量,然后进行赋值
 35      var num1 int
 36      num1 = 30
 37      fmt.Printf("num1的数值是:%d\n",num1)
 38      //写在一行
 39      var num2 int = 15
 40      fmt.Printf("num2的数值是:%d\n",num2)//printf
 41
 42      //第二种:类型推断
 43      var name = "王二狗"
 44      fmt.Printf("类型是:%T,数值是:%s\n",name,name)
 45
 46      //第三种,简短定义,也叫简短声明
 47      sum := 100
 48      fmt.Println(sum)
 49
 50      //多个变量同时定义
 51      var a, b, c int
 52      a = 1
 53      b = 2
 54      c = 3
 55      fmt.Println(a,b,c)
 56
 57      var m, n int = 100,200
 58      fmt.Println(m,n)
 59
 60      var n1,f1,s1 = 100,3.14,"Go"
 61      fmt.Println(n1,f1,s1)
 62
 63      var (
 64         studentName = "李小花"
 65         age = 18
 66         sex = "女"
 67      )
 68      fmt.Printf("学生姓名:%s,年龄:%d,性别:%s\n",studentName,age,sex)
 69 }

在这里插入图片描述

变量定义过程内存分析

跟c一样,使用go时要有时刻从内存分析的好习惯

1 package main
  2
  3 import "fmt"
  4
  5 var a = 1000 //全局变量
  6 var b int = 2000
  7 //c := 3000 //语法错误
  8 func main() {
    
    
  9     /*
 10     注意点:
 11     1.变量必须先定义才能使用
 12     2.变量的类型和赋值必须一致
 13     3.同一个作用域内,变量名不能冲突
 14     4.简短定义方式,左边的变量至少有一个是新的
 15     5.简短定义方式,不能定义全局变量
 16     6.变量的零值,就是默认值
 17         整型:默认值是0
 18         浮点类型:默认是0
 19         字符串:默认值""
 20      */
 21     var num int
 22     num = 100
 23     fmt.Printf("num的数值是:%d,地址是:%p\n",num, &num)
 24
 25     num = 200
 26     fmt.Printf("num的数值是:%d,地址是:%p\n",num, &num)
 27
 28     // fmt.Println(num2) //undefined: num2
 29
 30     var name string
 31     //name = 100
 32     //fmt.Println(name) //cannot use 100 (type int) as type string in assignment
 33     name = "王二狗"
 34     fmt.Println(name)
 35
 36     //var name string  = "李小花"
 37     //fmt.Println(name)
 38
 39     num ,name ,sex := 1000,"李小花" , "男" //no new variables on left side of :=
 40     fmt.Println(num,name,sex)
 41
 42     fmt.Println(a)
 43
 44     fmt.Println("----------------------")
 45     var m int //整数,默认值是0
 46     fmt.Println(m)
 47     var n float64 //0.0-->0
 48     fmt.Println(n)
 49     var s string //""
 50     fmt.Println(s)
 51     var s2 []int //切片[]
 52     fmt.Println(s2)
 53     fmt.Println(s2 == nil)
 54
 55     //var sum = 100 //sum declared and not used
 56
 57 }

在这里插入图片描述
定义变量时会给该变量分配一块内存地址,重新赋值变量是,变量的值改了,但变量的内存地址不变

常量

关键字是常量
用来定义一般不变的值

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         常量:
  8         1.概念:同变量类似,程序执行过程中数值不能改变
  9         2.语法:
 10             显式类型定义
 11             隐式类型定义,就是不指定类型
 12
 13         3.常数:
 14             固定的数值:100,"abc"
 15
 16
 17           跟变量不一样,常量定义了不使用不会报错
 18           常量值类型只能是布尔值,数字型和字符串型
 19     */
 20     fmt.Println(100)
 21     fmt.Println("hello")
 22
 23     //1.定义常量
 24     const PATH string = "http:www.baidu.com"
 25     const PI = 3.14
 26     fmt.Println(PATH)
 27     //fmt.Println(PI)
 28
 29     //2.尝试修改常量的数值
 30     //PATH = "http://www.sina.com" //cannot assign to PATH
 31
 32     //3.定义一组常量
 33     const C1, C2, C3 = 100, 3.14, "haha"
 34     const (
 35         MALE   = 0
 36         FEMALE = 1
 37         UNKNOW = 3
 38     )
 39     //4.一组常量中,如果某个常量没有初始值,默认和上一行一致,值和类型
 40     const (
 41         a int = 100
 42         b
 43         c string = "ruby"
 44         d
 45         e
 46     )
 47     fmt.Printf("%T,%d\n", a, a) //格式化标准输出用的格式符合变量一样
 48     fmt.Printf("%T,%d\n", b, b)
 49     fmt.Printf("%T,%s\n", c, c)
 50     fmt.Printf("%T,%s\n", d, d)
 51     fmt.Printf("%T,%s\n", e, e)
 52
 53     //5. 枚举类型:使用常量组作为枚举类型。一组相关数值的数据
 54     const (
 55         SPRING = 0
 56         SUMMER = 1
 57         AUTUMN = 2
 58         WINTER = 3
 59     )
 60
 61 }

在这里插入图片描述

iota关键字

新const,iota变0,新常量iota加一

 1 package main
  2
  3 import (
  4     "fmt"
  5 )
  6
  7 func main() {
    
    
  8     /*
  9     iota:特殊的常量,可以被编译器自动修改的常量
 10         每当定义一个const,iota的初始值为0
 11         每当定义一个常量,就会自动累加1
 12         直到下一个const出现,清零
 13      */
 14     const (
 15         a = iota // 0
 16         b = iota // 1
 17         c = iota //2
 18     )
 19     fmt.Println(a)
 20     fmt.Println(b)
 21     fmt.Println(c)
 22
 23     const (
 24         d = iota // 0
 25         e        // 1
 26     )
 27     fmt.Println(d)
 28     fmt.Println(e)
 29
 30     //枚举中
 31     const (
 32         MALE   = iota // 0
 33         FEMALE  // 1
 34         UNKNOW  // 2
 35     )
 36     fmt.Println(MALE, FEMALE, UNKNOW)
 37
 38 }

在这里插入图片描述

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     const (   #其实常量的定义时已经有iota关键字(有const后台必定在运算iota
  7         A = iota // 0
  8         B // 1
  9         C // 2
 10         D = "haha" // iota = 3
 11         E // haha iota = 4
 12         F = 100 //iota =5
 13         G //100 iota = 6
 14         H = iota // 7
 15         I //iota 8
 16     )
 17     const (
 18         J = iota // 0
 19     )
 20     fmt.Println(A)
 21     fmt.Println(B)
 22     fmt.Println(C)
 23     fmt.Println(D)
 24     fmt.Println(E)
 25     fmt.Println(F)
 26     fmt.Println(G)
 27     fmt.Println(H)
 28     fmt.Println(I)
 29     fmt.Println(J)
 30 }

在这里插入图片描述

基本数据类型

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     Go语言的数据类型:
  8     1.基本数据类型:
  9         布尔类型:bool
 10             取值:true,false
 11         数值类型:
 12             整数:int
 13                 有符号:最高位表示符号位,0正数,1负数,其余位表示数值
 14                     int8:(-128 到 127)
 15                     int16:(-32768 到 32767)
 16                     int32:(-2147483648 到 2147483647)
 17                     int64:(-9223372036854775808 到 9223372036854775807)
 18                 无符号:所有的位表示数值
 19                     uint8: (0255)
 20                     uint16:(065535)
 21                     uint32:(04294967295)
 22                     uint64: (018446744073709551615)
 23
 24                 int, uint
 25
 26                 byte:uint8
 27                 rune:int32
 28             浮点:生活中的小数
 29                 float32,float64
 30             复数:complex,
 31         字符串:string
 32     2.复合数据类型
 33         array,slice,map,function,pointer,struct,interface,channel。。。
 34      */
 35      //1.布尔类型
 36      var b1 bool
 37      b1 = true
 38      fmt.Printf("%T,%t\n",b1,b1)
 39      b2 :=false
 40      fmt.Printf("%T,%t\n",b2,b2)
 41
 42      //2.整数
 43      var i1 int8
 44      i1 = 100
 45      fmt.Println(i1)
 46      var i2 uint8
 47      i2 = 200
 48      fmt.Println(i2)
 49
 50      var i3 int
 51      i3 = 1000
 52      fmt.Println(i3)
 53      //语法角度:int,int64不认为是同一种类型
 54      //var i4 int64
 55      //i4 = i3 //cannot use i3 (type int) as type int64 in assignment
 56
 57      var i5 uint8
 58      i5 = 100
 59      var i6 byte
 60      i6 = i5
 61      fmt.Println(i5,i6)
 62
 63      var i7 = 100
 64      fmt.Printf("%T,%d\n",i7,i7)
 65
 66      //浮点
 67      var f1 float32
 68      f1 = 3.14
 69      var f2 float64
 70      f2 = 4.67
 71      fmt.Printf("%T,%.2f\n",f1,f1)//%T输出类型,.2表示小数点后两位,也会四舍五入,.前面标识总得显示长度(包括小数点),多了不空格在前面,少了正常长度显示如3.14就是4为长度
 72      fmt.Printf("%T,%.3f\n",f2,f2)
 73      fmt.Println(f1)
 74
 75      var f3 = 2.55
 76      fmt.Printf("%T\n",f3)
 77
 78
 79 }

一般直接用int,除非需要使用特定大小的整数,会根据你的系统来判断32位还是64位(x86表示32位,x84_64,x64就是64位)
int和int32或int64不是同一个东西

在这里插入图片描述

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         字符串:
  8         1.概念:多个byte的集合,理解为一个字符序列
  9         2.语法:使用双引号
 10             "abc","hello","A"
 11                 也可以使用``
 12         3.编码问题
 13                 计算机本质只识别0和1
 14                 A:65,B:66,C:67...
 15                 a:97,b:98...
 16             ASCII(美国标准信息交换码)
 17
 18             中国的编码表:gbk,兼容ASCII
 192021             Unicode编码表:号称统一了全世界
 22                 UTF-8,UTF-16,UTF-32...
 23
 24         4.转义字符:\
 25             A:有一些字符,有特殊的作用,可以转义为普通的字符
 26                 \',\'
 27             B:有一些字符,就是一个普通的字符,转义后有特殊的作用
 28                 \n,换行
 29                 \t,制表符
 30     */
 31     //1.定义字符串
 32     var s1 string
 33     s1 = "王二狗"
 34     fmt.Printf("%T,%s\n", s1, s1)
 35
 36     s2 := `Hello World`
 37     fmt.Printf("%T,%s\n", s2, s2)
 38
 39     //2.区别:'A',"A"
 40     v1 := 'A'
 41     v2 := "A"
 42     fmt.Printf("%T,%d\n", v1, v1) //int32
 43     fmt.Printf("%T,%s\n", v2, v2)
 44
 45     v3 := '中'
 46     fmt.Printf("%T,%d,%c,%q\n", v3, v3, v3, v3) //%c字符串输出,%q输出中文时有引号
 47
 48     //3.转义字符
 49     fmt.Println("\"HelloWorld\"")
 50     fmt.Println("Hello\nWor\tld")
 51
 52     fmt.Println(`He"lloWor"ld`)
 53     fmt.Println("Hello`Wor`ld")
 54 }

在这里插入图片描述

数据类型转换

其实是对变量的值进行转换,注意兼容类型才能转换,也可以用隐式定义

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         数据类型转换:Type Convert
  8         go语言是静态语言,定义,赋值,运算必须类型一致
  9
 10         语法格式:Type(Value)
 11
 12         注意点:兼容类型可以转换
 13
 14         常数:在有需要的时候,自动转型
 15         变量:需要手动转型
 16     */
 17     var a int8
 18     a = 10
 19
 20     var b int16
 21     b = int16(a)
 22     fmt.Println(a, b)
 23
 24     f1 := 4.83
 25     var c int
 26     c = int(f1)
 27     fmt.Println(f1, c)
 28
 29     f1 = float64(a)
 30     fmt.Println(f1, a)
 31
 32     //b1 := true
 33     //a = int8(b1) //cannot convert b1 (type bool) to type int8不是任意转换的,要是兼容类型
 34
 35     sum := f1 + 100
 36     fmt.Printf("%T,%f\n", sum, sum)
 37 }

在这里插入图片描述

运算符

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         算术运算符:+,-,*,/,%,++,--
  8         +
  9         -
 10         *,乘法
 11         /:取商,两个数相除,取商
 12         %:取余,取模,两个数相除,取余数
 13
 14         ++:给自己加1
 15             i++
 16         --:给自己减1
 17             i--
 18         自增自减运算符的使用要注意它的先后,比如i++是先自增1在整体表达式的值,++i是先整体表达式的值再到i的值自增一
 19         整数
 20     */
 21     a := 10
 22     b := 3
 23     sum := a + b
 24     fmt.Printf("%d + %d = %d\n", a, b, sum)
 25
 26     sub := a - b
 27     fmt.Printf("%d - %d = %d\n", a, b, sub)
 28
 29     mul := a * b
 30     fmt.Printf("%d * %d = %d\n", a, b, mul)
 31
 32     div := a / b //取商,或者说取整
 33     mod := a % b //取余
 34     fmt.Printf("%d / %d = %d\n", a, b, div)
 35     fmt.Printf("%d %% %d = %d\n", a, b, mod)
 36
 37     c := 3
 38     c++ //就是给c+1
 39     fmt.Println(c)
 40
 41     c-- //给c减1
 42     fmt.Println(c)
 43
 44 }//+= -= *= %= 取余取商

在这里插入图片描述

关系运算符

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     关系运算符:>,<,>=,<=,==,!=
  8         结果总是bool类型的:true,false
  9         ==:表示比较两个数值是相等的
 10         !=:表示比较两个数值是不相等的
 11      */
 12      a := 3
 13      b := 5
 14      c := 3
 15      res1 := a > b
 16      res2 := b > c
 17      fmt.Printf("%T,%t\n",res1,res1)
 18      fmt.Printf("%T,%t\n",res2,res2)
 19
 20      res3 := a == b
 21      fmt.Println(res3)
 22
 23      res4 := a == c
 24      fmt.Println(res4)
 25
 26      fmt.Println(a != b, a != c)
 27 }

在这里插入图片描述

逻辑运算符

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     逻辑运算符:操作数必须是bool,运算结果也是bool
  8     逻辑与:&&
  9         运算规则:所有的操作数都是真,结果才为真,有一个为假,结果就为假
 10             "一假则假,全真才真"
 11     逻辑或:||
 12         运算规则:偶有的操作数都是假,结果才为假,有一个为真,结果就为真
 13             "一真则真,全假才假"
 14     逻辑非:!
 15         !T-->false
 16         !F-->true
 17      */
 18     f1 := true
 19     f2 := false
 20     f3 := true
 21     res1 := f1 && f2
 22     fmt.Printf("res1: %t\n", res1)
 23
 24     res2 := f1 && f2 && f3
 25     fmt.Printf("res2: %t\n", res2)
 26
 27     res3 := f1 || f2
 28     fmt.Printf("res3: %t\n", res3)
 29     res4 := f1 || f2 || f3
 30     fmt.Printf("res4: %t\n", res4)
 31     fmt.Println(false || false || false)
 32
 33     fmt.Printf("f1:%t,!f1:%t\n", f1, !f1)
 34     fmt.Printf("f2:%t,!f2:%t\n", f2, !f2)
 35
 36     a := 3
 37     b := 2
 38     c := 5
 39     res5 := a > b && c%a == b && a < (c/b)//后边这个逻辑不用运算了,因为已经得出结果了
 40     fmt.Println(res5)
 41
 42     res6 := b*2 < c || a/b != 0 || c/a > b//当算第一个就知道res6的结果了,不用再计算
 43     fmt.Println(res6)
 44     res7:=!(c/a==b)
 45     fmt.Println(res7)
 46
 47 }

在这里插入图片描述

位运算符

二进制操作,按位操作
二进制的政府判断通过顶格的0或1,看时八位八位看
左移右移整个八位八位的移动,左移后末尾的补0,右移后超过基础位(个数位)的删除

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     位运算符:
  8         将数值,转为二进制后,按位操作
  9     按位&10         对应位的值如果都为1才为1,有一个为0就为0
 11     按位|12         对应位的值如果都是0才为0,有一个为1就为1
 13     异或^:
 14         二元:a^b
 15             对应位的值不同为1,相同为0
 16         一元:^a
 17             按位取反:
 18                 1--->0
 19                 0--->1
 20     位清空:&^
 21             对于 a &^ b
 22                 对于b上的每个数值
 23                 如果为0,则取a对应位上的数值
 24                 如果为1,则结果位就取0
 25
 26     位移运算符:
 27     <<:按位左移,将a转为二进制,向左移动b位
 28         a << b  ,就是将a放大了2的b次方
 29     >>: 按位右移,将a 转为二进制,向右移动b位
 30         a >> b
 31      */
 32
 33     a := 60
 34     b := 13
 35     /*
 36     a: 60 0011 1100
 37     b: 13 0000 1101
 38     &     0000 1100
 39     |     0011 1101
 40     ^     0011 0001
 41     &^    0011 0000
 42
 43
 44     a : 0000 0000  0011 1100//二进制的补0,一般四位一段开
 45     ^   1111 1111 ... 1100 0011
 46      */
 47     fmt.Printf("a:%d, %b\n",a,a)//%b二进制输出
 48     fmt.Printf("b:%d, %b\n",b,b)
 49
 50     res1 := a & b
 51     fmt.Println(res1) // 12
 52
 53     res2 := a | b
 54     fmt.Println(res2) // 61
 55
 56     res3 := a ^ b
 57     fmt.Println(res3) // 49
 58
 59     res4 := a &^ b
 60     fmt.Println(res4) // 48
 61
 62     res5 := ^a
 63     fmt.Println(res5)
 64
 65     c:=8
 66     /*
 67     c : ... 0000 1000
 68           0000 100000
 69     >>        0000 10
 70      */
 71     res6 := c << 2
 72     fmt.Println(res6)
 73
 74     res7 := c >> 2
 75     fmt.Println(res7)

赋值运算符

总得提一下运算符的优先级,一般都是一元运算符最高,记不住优先级没关系,需要的时候查看下就可以了,另外不建议写复杂度运算符的运算式子,可读性差,还有就是可以用括号来提高优先级

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     赋值运算符:
  8         =,+=,-=,*=,/=,%=,<<=,>>=,&=,|=,^=...
  9         =,把=右侧的数值,赋值给=左边的变量
 10
 11         +=, a += b,相当于a = a + b
 12             a++, a += 1
 13      */
 14      var a int
 15      a = 3
 16      fmt.Println(a)
 17
 18      a += 4 // a = a + 4
 19      fmt.Println(a) // 7
 20
 21      a -= 3
 22      fmt.Println(a) // 4
 23      a *= 2
 24      fmt.Println(a) // 8
 25      a /= 3
 26      fmt.Println(a) // 2
 27      a %= 1
 28      fmt.Println(a) // 0
 29
 30
 31
 32 }

在这里插入图片描述

标准输入和输出

各种占位符,这里只说常用的

 1 package main
  2
  3 import (
  4     "fmt"
  5
  6     "os"
  7     "bufio"
  8 )
  9
 10 func main() {
    
    
 11     /*
 12     输入和输出:
 13         fmt包:输入,输出
 14
 15         输出:
 16             Print() //打印
 17             Printf() //格式化打印
 18             Println() //打印之后换行
 19
 20         格式化打印占位符:
 21             %v,原样输出
 22             %T,打印类型
 23             %t,bool类型
 24             %s,字符串
 25             %f,浮点
 26             %d,10进制的整数
 27             %b,2进制的整数
 28             %o,8进制
 29             %x,%X,16进制
 30                 %x:0-9,a-f
 31                 %X:0-9,A-F
 32             %c,打印字符
 33             %p,打印地址
 34             。。。
 35
 36         输入:
 37             Scanln()//制度去一行
 38                 Scanln is similar to Scan, but stops scanning at a newline and after the final item there must be a newline or EOF.
 39             Scanf()
 40
 41         bufio包
 42
 43      */
 44     //a := 100           //int
 45     //b := 3.14          //float64
 46     //c := true          // bool
 47     //d := "Hello World" //string
 48     //e := `Ruby`        //string
 49     //f := 'A'
 50     //fmt.Printf("%T,%b\n", a, a)
 51     //fmt.Printf("%T,%f\n", b, b)
 52     //fmt.Printf("%T,%t\n", c, c)
 53     //fmt.Printf("%T,%s\n", d, d)
 54     //fmt.Printf("%T,%s\n", e, e)
 55     //fmt.Printf("%T,%d,%c\n", f, f, f)
 56     //fmt.Println("-----------------------")
 57     //fmt.Printf("%v\n", a)
 58     //fmt.Printf("%v\n", b)
 59     //fmt.Printf("%v\n", c)
 60     //fmt.Printf("%v\n", d)
 61     //fmt.Printf("%v\n", e)
 62     //fmt.Printf("%v\n", f)
 63     //
 64     //
 65     //fmt.Println("-----------------------")
 66
 67
 68     //var x int
 69     //var y float64
 70     //fmt.Println("请输入一个整数,一个浮点类型:")
 71     //fmt.Scanln(&x,&y)//读取键盘的输入,通过操作地址,赋值给x和y   阻塞式 这种情况下的赋值,中间要有空格
 72     //fmt.Printf("x的数值:%d,y的数值:%f\n",x,y)
 73     //
 74     //fmt.Scanf("%d,%f",&x,&y)//要用取地址符是通过操控变量的内存地址来复制给变量的,这种情况,要有逗号,跟他格式来定
 75     //fmt.Printf("x:%d,y:%f\n",x,y)
 76
 77     fmt.Println("请输入一个字符串:")
 78     reader := bufio.NewReader(os.Stdin)//从哪里读取,这里是标准输入,也就是键盘
 79     s1, _ := reader.ReadString('\n')//这里表示读到换行符结束_表示舍弃某个字符
 80     fmt.Println("读到的数据:", s1)
 81
 82 }

在这里插入图片描述

流程语句

if语句

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         条件语句:if
  8         语法格式:
  9             if 条件表达式{
    
    
 10                 //
 11             }
 12
 13      */
 14     //1.给定一个数字,如果大于10,我们就输出打印这个数字大于10
 15
 16     num := 6
 17     if num > 10 {
    
    
 18         fmt.Println("大于10")
 19     }
 20
 21
 22     //2.给定一个成绩,如果大于等于60分,就打印及格
 23     score := 18
 24     if score >= 60{
    
    
 25         fmt.Println(score,"成绩及格。。")
 26     }
 27     fmt.Println("main..over....")
 28 }

if语句判断成功就执行语句块的内容,不成功就不执行
判断成功就不继续后续的判断了,所以是有顺序的

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     if...else语句
  8         if 条件{
    
    
  9             //条件成立,执行此处的代码。。
 10             A段
 11         }else{
    
    
 12             //条件不成立,执行此处的代码。。
 13             B段
 14         }
 15
 16     注意点:
 17         1.if后的{
    
    ,一定是和if条件写在同一行的
 18         2.else一定是if语句}之后,不能自己另起一行
 19         3.if和else中的内容,二者必选其一的执行
 20      */
 21     //给定一个成绩,如果大于等于60,就是及格,否则呢就是不及格
 22     /*
 23     score := 0
 24     fmt.Println("请输入您的成绩:")
 25     fmt.Scanln(&score)
 26
 27     if score >= 60{
    
    
 28         fmt.Println(score,"及格。。")
 29     }else{
    
    
 30         fmt.Println(score,"不及格。。")
 31     }
 32 */
 33     //给定性别,如果是男,就去男厕所,否则去女厕所
 34     sex := "女"//bool, int, string
 35
 36     if sex == "男"{
    
    
 37         fmt.Println("可以去男厕所啦。。。")
 38     } else{
    
    
 39         fmt.Println("你去女厕所吧。。")
 40     }
 41
 42     fmt.Println("main...over....")
 43 }

注意else的位置,如果不熟练,以在vim中用:GoFmt来格式化代码即可

下面是if else语句的嵌套

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     if语句的嵌套:
  8     if 条件1{
    
    
  9         A段
 10     }else{
    
    
 11         if 条件2{
    
    
 12             B段
 13         }else{
    
    
 14             C段
 15         }
 16     }
 17
 18     简写:
 19     if 条件1{
    
    
 20         A段 //条件1成立
 21     }else if 条件2{
    
    
 22         B段 // 条件1不成立,条件2成立
 23     }else if 条件3{
    
    
 24         C段 // 条件1不成立,条件2不成立,条件3成立。。
 25
 26     }.。。else{
    
    
 27     }
 28
 29      */
 30
 31     sex := "泰国" //bool, int, string
 32
 33     if sex == "男" {
    
    
 34         fmt.Println("可以去男厕所啦。。。") // sex 是男
 35     } else if sex == "女" {
    
    
 36         fmt.Println("你去女厕所吧。。") //sex 是女
 37
 38     } else {
    
    
 39         fmt.Println("我也不知道了。。")
 40
 41     }
 42
 43     fmt.Println("main...over....")
 44 }

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     if语句的其他写法:
  8     if 初始化语句; 条件{
    
    
  9         //注意变量的作用域问题
 10     }
 11      */
 12
 13      if num := 4; num > 0{
    
    
 14         fmt.Println("正数。。",num)
 15      }else if num < 0{
    
    
 16         fmt.Println("负数。。",num)
 17      }
 18      //fmt.Println(num) // undefined: num
 19
 20      num2 := 5
 21      if num2 > 0{
    
    
 22         fmt.Println("num2,是正数。。",num2)
 23      }
 24      fmt.Println(num2)
 25
 26 }

这就是在if语句的判断位置初始化变量,这个变量的作用于是if语句,在走if else整个流程中生效,完成了,变量就销毁了,也就是内存空间收回来了

(if else语句的判断条件是有先后顺序的,变量的作用域或者它的作用效果,不能通过在代码中的位置的顺序判断,而是看变量类型来确定作用于)

switch语句

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     switch语句:
  8     语法结构:
  9         switch 变量名{
    
    
 10         case 数值1:分支1
 11         case 数值2:分支2
 12         case 数值3:分支3
 13         。。。。
 14         default:
 15             最后一个分支
 16         }
 17     注意事项:
 18         1.switch可以作用在其他类型上,case后的数值必须和switch作用的变量类型一致
 19         2.case是无序的
 20         3.case后的数值是唯一的(duplicate case 3 in switch)
 21         4.default语句是可选的操作
 22      */
 23     num := 3 //int
 24     switch num {
    
    
 25     default:
 26         fmt.Println("数据有误。。。")
 27     case 3:
 28         fmt.Println("第三季度")
 29     case 4:
 30         fmt.Println("第四季度")
 31     case 1:
 32         fmt.Println("第一季度")
 33     case 2:
 34         fmt.Println("第二季度")
 35
 36     }
 37
 38     //模拟计算器
 39     num1 := 0
 40     num2 := 0
 41     oper := ""
 42     fmt.Println("请输入一个整数:")
 43     fmt.Scanln(&num1)
 44     fmt.Println("请再次输入一个整数:")
 45     fmt.Scanln(&num2)
 46     fmt.Println("请输入一个操作:+,-,*,/")
 47     fmt.Scanln(&oper)
 48
 49     switch oper {
    
    
 50     case "+":
 51         fmt.Printf("%d + %d = %d\n", num1, num2, num1+num2)
 52     case "-":
 53         fmt.Printf("%d - %d = %d\n", num1, num2, num1-num2)
 54     case "*":
 55         fmt.Printf("%d * %d = %d\n", num1, num2, num1*num2)
 56     case "/":
 57         fmt.Printf("%d / %d = %d\n", num1, num2, num1/num2)
 58     }
 59
 60     fmt.Println("main... over ...")
 61 }

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         1.switch的标注写法:
  8         switch 变量{
    
    
  9         case 数值1:分支1
 10         case 数值2:分支2
 11         。。。
 12         default:
 13             最后一个分支
 14         }
 15         2.省略switch后的变量,相当于直接作用在true上
 16         switch{
    
    //true
 17         case true:
 18         case false:
 19         }
 20
 21         3.case后可以同时跟随多个数值
 22         switch 变量{
    
    
 23         case 数值1,数值2,数值3:
 24
 25         case 数值4,数值5:
 26
 27         }
 28
 29         4.switch后可以多一条初始化语句
 30         switch 初始化语句;变量{
    
    
 31         }
 32     */
 33     switch {
    
    
 34     case true:
 35         fmt.Println("true..")
 36     case false:
 37         fmt.Println("false...")
 38     }
 39     /*
 40         成绩:
 41         [0-59],不及格
 42         [60,69],及格
 43         [70,79],中
 44         [80,89],良好
 45         [90,100],优秀
 46     */
 47     score := 88
 48     switch {
    
    
 49     case score >= 0 && score < 60:
 50         fmt.Println(score, "不及格")
 51     case score >= 60 && score < 70:
 52         fmt.Println(score, "及格")
 53     case score >= 70 && score < 80:
 54         fmt.Println(score, "中等")
 55     case score >= 80 && score < 90:
 56         fmt.Println(score, "良好")
 57     case score >= 90 && score <= 100:
 58         fmt.Println(score, "优秀")
 59     default:
 60         fmt.Println("成绩有误。。。")
 61
 62     }
 63
 64     fmt.Println("---------------------")
 65     letter := ""
 66     switch letter {
    
    
 67     case "A", "E", "I", "O", "U":
 68         fmt.Println(letter, "是元音。。")
 69     case "M", "N":
 70         fmt.Println("M或N。。")
 71     default:
 72         fmt.Println("其他。。")
 73     }
 74     /*
 75         一个月的天数
 76         1,3,5,7,8,10,12
 77             31
 78         4,6,9,11
 79             30
 80         2:29/28
 81     */
 82     month := 9
 83     day := 0
 84     year := 2019
 85     switch month {
    
    
 86     case 1, 3, 5, 7, 8, 10, 12:
 87         day = 31
 88
 89     case 4, 6, 9, 11:
 90         day = 30
 91     case 2:
 92         if year%400 == 0 || year%4 == 0 && year%100 != 0 {
    
    
 93             day = 29
 94         } else {
    
    
 95             day = 28
 96         }
 97     default:
 98         fmt.Println("月份有误。。")
 99     }
100     fmt.Printf("%d 年 %d 月 的天数是:%d\n", year, month, day)
101     fmt.Println("--------------------------")
102
103     switch language := "golang"; language {
    
     //作用于也是switch内
104     case "golang":
105         fmt.Println("Go语言。。")
106     case "java":
107         fmt.Println("Java语言。。")
108     case "python":
109         fmt.Println("Python语言。。")
110     }
111     //fmt.Println(language) //undefined: language
112 }

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     switch中的break和fallthrough语句
  8     break:可以使用在switch中,也可以使用在for循环中
  9         强制结束case语句,从而结束switch分支
 10
 11     fallthrough:用于穿透switch
 12         当switch中某个case匹配成功之后,就执行该case语句
 13         如果遇到fallthrough,那么后面紧邻的case,无需匹配, 执行穿透执行。
 14
 15         fallthrough应该位于某个case的最后一行
 16      */
 17       n := 2
 18     switch n {
    
    
 19     case 1:
 20         fmt.Println("我是熊大")
 21         fmt.Println("我是熊大")
 22         fmt.Println("我是熊大")
 23     case 2:
 24         fmt.Println("我是熊二")
 25         fmt.Println("我是熊二")
 26         break //用于强制结束case,意味着switch被强制结束
 27         fmt.Println("我是熊二")
 28     case 3:
 29         fmt.Println("我是光头强")
 30         fmt.Println("我是光头强")
 31         fmt.Println("我是光头强")
 32     }
 33
 34     fmt.Println("-----------------")
 35     m := 2
 36     switch m {
    
    
 37     case 1:
 38         fmt.Println("第一季度。。")
 39     case 2:
 40         fmt.Println("第二季度。。")
 41         fmt.Println("...")
 42         fallthrough
 43     case 3:
 44         fmt.Println("第三季度。。")
 45         fallthrough
 46     case 4:
 47         fmt.Println("第四季度。。")
 48
 49
 50     }
 51     fmt.Println("main...over...")
 52 }

swith语句默认语句块的后面有break

if和switch属于选择分支语句

for循环语句

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         for循环:某些代码会被多次的执行
  8         语法:
  9             for 表达式1;表达式2;表达式3{
    
    
 10                 循环体
 11             }
 12     */
 13     //fmt.Println("hello world..")
 14     //fmt.Println("hello world..")
 15     //fmt.Println("hello world..")
 16     //fmt.Println("hello world..")
 17     //fmt.Println("hello world..")
 18
 19     for i := 1; i <= 5; i++ {
    
     //判断成功之后,i++,作用域是for循环语句
 20         fmt.Println("hello world..") // 1,2,3,4,5
 21     }
 22 }

3个表达式,i=1只执行一次,表达式二和表达式三轮流

下面是for循环的特殊写法,其实就是基于三个表达式的可省略性来写的,建议大家还是以使用常规写法为主
语句范围内的变量的取名也要注意全局变量等变量的影响,总之整份代码,不论作用于如何,变量名都不要有同样的情况

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     1. 标准写法:
  8         for 表达式1;表达式2;表达式3{
    
    
  9             循环体
 10         }
 11     2.同时省略表达式1和表达式3
 12         for 表达式2{
    
    
 13
 14         }
 15     相当于while(条件)
 16     3.同时省略3个表达式
 17         for{
    
    
 18
 19         }
 20     相当于while(true)
 21     注意点:当for循环中,省略了表达式2,就相当于直接作用在了true上
 22
 23     4.其他的写法:for循环中同时省略几个表达式都可以。。
 24         省略表达式1:
 25         省略表达式2:循环永远成立-->死循环
 26         省略表达式3:
 27      */
 28     i := 1
 29     for i <= 5 {
    
    
 30         fmt.Println(i)
 31         i++
 32     }
 33     fmt.Println("-->",i)
 34
 35     fmt.Println("------------")
 36     for{
    
    
 37         fmt.Println("i--->",i)
 38         i++
 39     }
 40 }

几个for循环练习
注意一个实用技巧或者说代码逻辑辅助思维,计数器

1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     for循环的练习题:
  8     练习1:打印58-23数字
  9     练习2:求1-100的和
 10         sum := 0
 11         sum = +1+2+3+4...+100
 12     sum = sum + 1
 13     sum = sum + 2
 14     sum = sum + 3
 15     ...
 16     sum = sum + 100
 17
 18     练习3:打印1-100内,能够被3整除,但是不能被5整除的数字,统计被打印的数字的个数,每行打印5个
 19      */
 20     for i := 58; i >= 23; i-- {
    
    
 21         fmt.Println(i) // 58,57,56...23
 22     }
 23
 24     fmt.Println("------------------")
 25     sum := 0
 26
 27     for i := 1;i <= 100;i++{
    
    
 28         sum += i // i : 1,2,3,4..100
 29     }
 30     fmt.Println("1-100的和:",sum)
 31
 32     fmt.Println("-----------------")
 33     count := 0 //计数器
 34     for i := 1;i <= 100;i++{
    
    
 35         if i % 3 == 0 && i % 5 != 0{
    
    
 36             fmt.Print(i,"\t")
 37             count++ //5, 10, 15, 20..
 38             if count % 5 == 0{
    
    
 39                 fmt.Println()
 40             }
 41         }
 42     }
 43     fmt.Println()
 44     fmt.Println("count-->",count)
 45 }

for嵌套

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     循环嵌套:多层循环嵌套在一起
  8
  9     题目一:
 10     *****
 11     *****
 12     *****
 13     *****
 14     *****
 15
 16     Print()
 17     Printf()
 18     Println()
 19
 20     题目二:
 21     1X1=1
 22     2X1=2 2X2=4
 23     3X1=3 3X2=6 3X3=9
 24     ...
 25     9X1=9 9X2=18 9X3=27...9X9=81
 26      */
 27     //第一行
 28     //1.打印*
 29     //for j := 0; j < 5; j++ {
    
    
 30     //
 31     //  fmt.Print("*") // j:0,1,2,3,4
 32     //}
 33     2.换行
 34     //fmt.Println()
 35
 36     第二行
 37     //for j := 0; j < 5; j++ {
    
    
 38     //
 39     //  fmt.Print("*") // j:0,1,2,3,4
 40     //}
 41     2.换行
 42     //fmt.Println()
 43
 44     for i := 1; i <= 5; i++ {
    
     //1,2,3,4,5
 45         for j := 0; j < 5; j++ {
    
    
 46
 47             fmt.Print("*") // j:0,1,2,3,4
 48         }
 49         //2.换行
 50         fmt.Println()
 51     }
 52 }

打印乘法表

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     题目二:                                i   j
  8     1X1=1                               1   1
  9     2X1=2 2X2=4                         2   1,2
 10     3X1=3 3X2=6 3X3=9                   3   1,2,3
 11     ...                                 ...
 12     9X1=9 9X2=18 9X3=27...9X9=81        9   1,2,3,...9
 13
 14
 15     i X j = ...
 16      */
 17     第一行
 18     //for j := 1; j <= 1; j++ {
    
    
 19     //  fmt.Printf("%d X %d = %d\t", 1, j, 1)
 20     //}
 21     //fmt.Println()
 22     第二行
 23     //for j := 1; j <= 2; j++ {
    
    
 24     //  fmt.Printf("%d X %d = %d\t", 2, j, 2*j)
 25     //}
 26     //fmt.Println()
 27     第三行
 28     //for j := 1; j <= 3; j++ {
    
    
 29     //  fmt.Printf("%d X %d = %d\t", 3, j, 2*j)
 30     //}
 31     //fmt.Println()
 32
 33     for i := 1; i < 10; i++ {
    
    
 34         for j := 1; j <= i; j++ {
    
    
 35             fmt.Printf("%d X %d = %d\t", j, i, i*j)
 36         }
 37         fmt.Println()
 38     }
 39
 40 }

两个规律增加的数,两个都能独立用for循环解决,那就嵌套一块
循环可以设计多种简单问题的代码方式

使用break或continue结束循环

break也可以用在switch中

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7         循环结束:
  8             循环条件不满足,循环自动结束了
  9             但是可以通过break和continue来强制的结束循环
 10
 11     循环控制语句
 12     break:彻底的结束循环。。终止
 13     continue:结束了某一次循环,下次继续。。中止
 14
 15
 16     注意点:多层循环嵌套,break和continue,默认结束的是里层循环
 17         如果想结束指定的某个循环,可以给循环贴标签(起名)18             break 循环标签名
 19             continue 循环标签名
 20 ¡
 21      */
 22
 23     for i := 1; i <= 10; i++ {
    
    
 24         if i == 5{
    
    
 25             //break
 26             continue
 27         }
 28         fmt.Println(i)
 29     }
 30
 31     fmt.Println("-----------------------")
 32     out:for i:=1;i<=5;i++{
    
    
 33         for j:=1;j<=5;j++{
    
    
 34             if j == 2{
    
    
 35                 break out
 36                 //continue out
 37             }
 38             fmt.Printf("i:%d,j:%d\n",i,j)
 39         }
 40     }
 41
 42     fmt.Println("main..over....")
 43 }

经典题for循环求水仙花数

 1 package main
  2
  3 import (
  4     "math"
  5     "fmt"
  6 )
  7
  8 func main() {
    
    
  9     /*
 10      水仙花数:三位数:[100,999]
 11         每个位上的数字的立方和,刚好等于该数字本身。那么就叫水仙花数,4个
 12         比如:153
 13             1*1*1 + 5*5*5 + 3*3*3 = 1+125+27=153
 14
 15         268
 16             268 /100 = 2
 17             268 % 10 = 8
 18
 19             268 -->26  %10 = 6
 20             268 -->68  /10 =6
 21      */
 22
 23     for i := 100; i < 1000; i++ {
    
    
 24         x := i / 100     //百位
 25         y := i / 10 % 10 //十位
 26         z := i % 10      //个位
 27
 28         if math.Pow(float64(x), 3)+math.Pow(float64(y), 3)+math.Pow(float64(z), 3) == float64(i) {
    
    
 29             fmt.Println(i)
 30         }
 31     }
 32     fmt.Println("------------------------")
 33     /*
 34     百位:1-9
 35     十位:0-9
 36     个位:0-9
 37      */
 38     for a := 1; a < 10; a++ {
    
    
 39         for b := 0; b < 10; b++ {
    
    
 40             for c := 0; c < 10; c++ {
    
    
 41                 n := a*100 + b*10 + c*1
 42                 if a*a*a+b*b*b+c*c*c == n {
    
    
 43                     fmt.Println(n)
 44
 45                 }
 46             }
 47         }
 48     }
 49
 50 }//math.pow()返回x的y次幂,math就是个包,标准包,pow()是他的源码文件,我们而言就是库源码文件

经典题求素数

 1 package main
  2
  3 import (
  4     "fmt"
  5     "math"
  6 )
  7
  8 func main() {
    
    
  9     /*
 10      打印2-100内的素数(只能被1和本身整除)
 11
 12
 13         2,3,5,7,11,13,17.。。。。
 14
 15         716             2,3,4,5,6
 17
 18         819             2
 20      */
 21     for i := 2; i <= 100; i++ {
    
    
 22         flag := true //记录i是否是素数
 23         for j := 2; j <= int(math.Sqrt(float64(i))); j++ {
    
     //判断到根号i就可以,不需要到i的前一个
 24             if i%j == 0 {
    
    
 25                 flag = false //不是不素了
 26                 break
 27             }
 28         }
 29
 30         if flag {
    
     //== true
 31             fmt.Println(i)
 32         }
 33     }
 34
 35 }

学go如果是为了云上发展,搞微服务之类,上面这些题不用化过多时间,过一遍就可以,主要是为了帮助理解for循环

goto语句

 1 package main
  2
  3 import "fmt"
  4
  5 func main() {
    
    
  6     /*
  7     goto语句:
  8
  9      */
 10
 11     var a = 10
 12 LOOP:
 13     for a < 20 {
    
    
 14         if a == 15 {
    
    
 15             a += 1
 16             goto LOOP
 17         }
 18         fmt.Printf("a的值为:%d\n", a)
 19         a++
 20     }
 21
 22     fmt.Println("----------------")
 23     for i := 0; i < 10; i++ {
    
    
 24         for j := 0; j < 10; j++ {
    
    
 25             if j == 2 {
    
    
 26                 goto breakHere
 27             }
 28         }
 29     }
 30     //手动返回,避免执行进入标签。。
 31     return
 32
 33 breakHere:
 34     fmt.Println("done...")
 35 }

一句话就是跳转到标签的,使用goto作为后备选,因为goto对代码逻辑影响比较大,一般用于跳出错误

数组和切片

生成随机数

随机数的生成依靠的是种子,所以种子如果相同那么生成的随机数也就相同,用seed来设置种子

 1 package main
  2
  3 import (
  4     "fmt"
  5     "math/rand"
  6 )
  7
  8 func main() {
    
    
  9     /*
 10         生成随机数random:其实是伪随机数,一位使用一定的算法公式算出来的
 11         math.rand(可以用之前学的python来理解,包下,源码,文件,模块或者说函数
 12     */
 13
 14     num1 := rand.Int() //随机整数
 15     fmt.Println(num1)
 16
 17     //范围比较大
 18     //指定范围
 19     for i := 0; i < 10; i++ {
    
    
 20         num := rand.Intn(10) //加上个指定范围的功能的,rand.Intn(n)表示范围是[0,n)
 21         fmt.Println(num)
 22     }
 23     rand.Seed(1) //手动更改可以改变随机数值,但这样不够自动化
 24     num2 := rand.Intn(10)
 25     fmt.Println(num2)
 26
 27     //时间每分每秒不一样,根据时间来设置seed
 28     t1 := time.now() //获取当前的时间
 29     fmt.Println(t1)
 30     fmt.Printf("%T", t1) //打印t1的类型,可见是time类型
 31     //seed要用的是整形,所以将时间转换成整形
 32     //时间戳,距离1970.1.1 0点0分0秒,单位:s
 33     timestamp1 := t1.unix()
 34     fmt.Println(timestamp1)
 35
 36     //第一步生成种子
 37     rand.Seed(Time.now().unix())
 38     for i := 0; i < 10; i++ {
    
    
 39         fmt.Println(rand.Intn(100))
 40     }
 41
 42 }

array(复合类型数据)

基础类型复合起来,就是复合数据类型

package main

import "fmt"

func main() {
    
    
	/*
		数据类型:
			基本类型:整数,浮点,布尔,字符串
			复合类型:array,slice,map,struct,pointer,function,channel。。。

		数组:
			1.概念:存储一组相同数据类型的数据结构
					理解为容器,存储一组数据
			2.语法:
					var 数组名 [长度] 数据类型
					var 数组名 = [长度] 数据类型{
    
    元素1,元素2.。。}
					数组名 := [...]数据类型{
    
    元素。。。}

			3.通过下标访问
				下标,也叫索引:index,
				默认从0开始的整数,直到长度减1
				数组名[index]
					赋值
					取值

				不能越界:[0,长度-1]

			4.长度和容量:go语言的内置函数
				len(array/map/slice/string),长度
				cap(),容量
	 */

	 var num1 int
	 num1 = 100

	 num1 = 200
	 fmt.Println(num1)
	 fmt.Printf("%p\n",&num1)

	 //step1:创建数组
	 var arr1 [4] int
	 fmt.Printf("%p\n",&arr1)
	 //step2:数组的访问
	 arr1[0] = 1
	 arr1[1] = 2
	 arr1[2] = 3
	 arr1[3] = 4
	 fmt.Println(arr1[0])//打印第一个数值
	 fmt.Println(arr1[2])//打印第三个数值
	 //fmt.Println(arr1[4]) //invalid array index 4 (out of bounds for 4-element array)

	 fmt.Println("数组的长度:",len(arr1)) //容器中实际存储的数据量
	 fmt.Println("数组的容量:",cap(arr1)) //容器中能够存储的最大的数量
	 //因为数组定长,长度和容量相同
	 arr1[0] = 100
	 fmt.Println(arr1[0])

	//数组的其他创建方式
	var a [4] int //同 var a= [4] int
	fmt.Println(a)

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

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

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

	var e = [5]string{
    
    "rose","王二狗","ruby"}
	fmt.Println(e)

	f := [...]int{
    
    1,2,3,4,5}
	fmt.Println(f)
	fmt.Println(len(f))
	g:=[...]int{
    
    1:3,6:5}
	fmt.Println(g)
	fmt.Println(len(g))
}

数组长度就当成数组存储的数组元素的个数来理解
在这里插入图片描述
与变量类似,数组创建也是开内存,数组的地址与数组第一个元素的地址相同
通过数组名字找到数组内存地址,在通过索引确定数组的第几个元素
不用了的,go有自己的垃圾回收来回收内存
数组关键字也是var

for range

package main

import "fmt"

func main() {
    
    
	/*
	数组的遍历:
		依次访问数组中的元素
		方法一:arr[0],arr[1],arr[2]....

		方法二:通过循环,配合下标
			for i:=0;i<len(arr);i++{
    
    
				arr[i]
			}
		方法三:使用range
			range,词义"范围"
			不需要操作数组的下标,到达数组的末尾,自动结束for range循环。
				每次都数组中获取下标和对应的数值。

	 */
	 arr1 := [5]int{
    
    1,2,3,4,5}
	 fmt.Println(arr1[0])
	 fmt.Println(arr1[1])
	 fmt.Println(arr1[2])
	 fmt.Println(arr1[3])
	 fmt.Println(arr1[4])

	 fmt.Println("---------------")
	 for i:=0;i<len(arr1);i++{
    
    
	 	arr1[i] = i*2+1
	 	fmt.Println(arr1[i])
	 }
	 fmt.Println(arr1)

	 fmt.Println("----------------")
	 for index,value := range arr1{
    
    
	 	fmt.Printf("下标是:%d,数值是:%d\n",index,value)
	 }

	 sum := 0
	 for _,v:=range arr1{
    
    
	 	sum += v
	 }
	 fmt.Println(sum)
}

在这里插入图片描述

值类型

数组与变量都是值类型,数组与数组间的传递是值传递

还有一种类引用型,理解成存储的不是值,而是存储内存地址

package main

import "fmt"

func main() {
    
    
	/*
		数据类型:
			基本类型:int,float,string,bool。。
			复合类型:array,slice,map,function,pointer,channel。。

		数组的数据类型:
			[size]type

		值类型:理解为存储的数值本身
			将数据传递给其他的变量,传递的是数据的副本(备份)
				int,float,string,bool,array
		引用类型:理解为存储的数据的内存地址
				slice,map。。
	 */

	//1.数据类型
	num := 10
	fmt.Printf("%T\n", num)

	arr1 := [4]int{
    
    1, 2, 3, 4}
	arr2 := [3]float64{
    
    2.15, 3.18, 6.19}
	arr3 := [4]int{
    
    5, 6, 7, 8}
	arr4 := [2]string{
    
    "hello", "world"}
	fmt.Printf("%T\n", arr1) //[4]int
	fmt.Printf("%T\n", arr2) //[3]float64
	fmt.Printf("%T\n", arr3) //[4]int
	fmt.Printf("%T\n", arr4) //[2]string

	//2.赋值
	num2 := num //值传递
	fmt.Println(num, num2) //10 10
	num2 = 20
	fmt.Println(num, num2) //10 20

	//数组呢
	arr5 := arr1 //值传递
	fmt.Println(arr1)
	fmt.Println(arr5)

	arr5[0] =1
	fmt.Println(arr1)
	fmt.Println(arr5)

	a := 3
	b := 4
	fmt.Println(a==b) //比较a和b的数值是否相等
	fmt.Println(arr5 == arr1) //比较数组的对应下标位置的数值是否相等
	//fmt.Println(arr1 == arr2) //invalid operation: arr1 == arr2 (mismatched types [4]int and [3]float64)

}

在这里插入图片描述

数组排序

package main

import "fmt"

func main() {
    
    
	/*
	数组的排序:
		让数组中的元素具有一定的顺序。

		arr :=[5]int{
    
    15,23,8,10,7}
			升序:[7,8,10,15,23]
			将序:[23,15,10,8,7]

	排序算法:
		冒泡排序,插入排序,选择排序,希尔排序,堆排序,快速排序。。。。

	冒泡排序:(Bubble Sort)
		依次比较两个相邻的元素,如果他们的顺序(如从大到小)就把他们交换过来。
	 */
	arr := [5]int{
    
    15, 23, 8, 10, 7}
	第一轮排序
	//for j := 0; j < 4; j++ {
    
    
	//	if arr[j] > arr[j+1] {
    
    
	//		arr[j], arr[j+1] = arr[j+1], arr[j]
	//	}
	//}
	//fmt.Println(arr)
	//
	第二轮排序
	//for j:=0;j<3;j++{
    
    
	//	if arr[j] > arr[j+1] {
    
    
	//		arr[j], arr[j+1] = arr[j+1], arr[j]
	//	}
	//}
	//fmt.Println(arr)

	for i:=1;i<len(arr);i++{
    
    
		for j := 0; j < len(arr)-i; j++ {
    
    
			if arr[j] > arr[j+1] {
    
    
				arr[j], arr[j+1] = arr[j+1], arr[j]
			}
		}
		fmt.Println(arr)
	}


在这里插入图片描述
理解算法,先把流程写下来在用代码实现
实际上冒泡排序和很多学校学的排序当下生产环境很少用这些了

多维数组

package main

import "fmt"

func main() {
    
    
	/*

		一维数组:存储的多个数据是数值本身
			a1 :=[3]int{
    
    1,2,3}

		二维数组:存储的是一维的一维
			a2 := [3][4]int{
    
    {
    
    },{
    
    },{
    
    }}

			该二维数组的长度,就是3。
			存储的元素是一维数组,一维数组的元素是数值,每个一维数组长度为4。

		多维数组:。。。


	*/
	a2 := [3][4]int{
    
    {
    
    1, 2, 3, 4}, {
    
    5, 6, 7, 8}, {
    
    9, 10, 11, 12}}
	fmt.Println(a2)
	fmt.Printf("二维数组的地址:%p\n", &a2)
	fmt.Printf("二维数组的长度:%d\n", len(a2))

	fmt.Printf("一维数组的长度:%d\n", len(a2[0]))
	fmt.Println(a2[0][3]) // 4
	fmt.Println(a2[1][2]) //7
	fmt.Println(a2[2][1]) // 10

	//遍历二维数组
	for i := 0; i < len(a2); i++ {
    
    
		for j := 0; j < len(a2[i]); j++ {
    
    
			fmt.Print(a2[i][j], "\t")
		}
		fmt.Println()
	}
	fmt.Println("---------------------")
	//for range 遍历二维数组
	for _, arr := range a2 {
    
    
		for _, val := range arr {
    
    
			fmt.Print(val, "\t")
		}
		fmt.Println()
	}
}

在这里插入图片描述
在这里插入图片描述

slice(引用类型)

数组一旦定义了或者说赋值了(因为有时候数组时还不指定长度),那么数组长度就固定了
而切片,你可以把它理解成动态数组
切片是一个引用类型的容器,指向底层的数组

对于切片,建议使用make来创造,make专门用来创造引用类型的数据

数组是定长的,所以len()和cap()一样

切片的长度表示已经存放了多少个数据,容量表示最多存放多少个数据

package main

import "fmt"

func main() {
    
    
	/*
	数组array:
		存储一组相同数据类型的数据结构。
			特点:定长

	切片slice:
		同数组类似,也叫做变长数组或者动态数组。
			特点:变长

		是一个引用类型的容器,指向了一个底层数组。

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

		第一个参数:类型
			slice,map,chan
		第二个参数:长度len
			实际存储元素的数量
		第三个参数:容量cap
			最多能够存储的元素的数量


	append(),专门用于向切片的尾部追加元素
		slice = append(slice, elem1, elem2)
		slice = append(slice, anotherSlice...)
	 */
	 //1.数组
	 arr := [4]int{
    
    1,2,3,4}//定长
	 fmt.Println(arr)

	 //2.切片
	 var s1 []int   //这样定义切片,默认是空
	 fmt.Println(s1)

	 s2 := []int{
    
    1,2,3,4} //变长
	 fmt.Println(s2)
	 fmt.Printf("%T,%T\n",arr,s2) //[4]int,[]int

	 s3 := make([]int,3,8)   //这样定义切片,默认值是[0,0,0]
	 fmt.Println(s3)
	 fmt.Printf("容量:%d,长度:%d\n",cap(s3),len(s3))
	 s3[0] = 1
	 s3[1] = 2
	 s3[2] = 3
	 fmt.Println(s3)
	 //fmt.Println(s3[3]) //panic: runtime error: index out of range

	 //append()//向切片末尾添加元素
	 s4 := make([]int,0,5)
	 fmt.Println(s4)
	 s4 = append(s4,1,2)//append操作切片,可能会使切片扩容,所以要让切片s4重新指向
	 fmt.Println(s4)
	 s4 = append(s4,3,4,5,6,7)
	 fmt.Println(s4)

	 s4 = append(s4,s3...)//s3...表示这不是s3而是s3的元素(注意元素类型)
	 fmt.Println(s4)


	 //遍历切片
	 for i:=0;i<len(s4);i++{
    
    //注意这里是长度
	 	fmt.Println(s4[i])
	 }

	 for i,v :=range s4{
    
    
	 	fmt.Printf("%d-->%d\n",i,v)
	 }

}

切片数组变量等等,在go语言中定义了就要用,不用的就不定义,否则会报错
超过容量没关系,会自动扩容

切片调用的index以长度为主,而不是看容量,如果硬是要操作超过长度的索引值,得用append

在这里插入图片描述

切片内存分析

package main

import "fmt"

func main() {
    
    
	/*
	切片Slice:
		1.每一个切片引用了一个底层数组
		2.切片本身不存储任何数据,都是这个底层数组存储,所以修改切片也就是修改这个数组中的数据
		3.当向切片中添加数据时,如果没有超过容量,直接添加,如果超过容量,自动扩容(成倍增长)
		4.切片一旦扩容,就是重新指向一个新的底层数组

	s1:3--->6--->12--->24

	s2:4--->8--->16--->32....

	 */
	s1 := []int{
    
    1, 2, 3}//切片是个引用类型,值是内存地址,在这里其实先创建了个底层数组,在将数组的地址赋值给s1
	fmt.Println(s1)
	fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:3,cap:3
	fmt.Printf("%p\n", s1)//注意,%p打印地址值,s1表示就是地址(值是地址,你可以直接把它看成地址)&s1表示它自身的地址

	s1 = append(s1, 4, 5)
	fmt.Println(s1)
	fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:5,cap:6
	fmt.Printf("%p\n", s1)//扩容了,成倍扩容,地址发生变化了,一般都是按乘2来算

	s1 = append(s1,6,7,8)
	fmt.Println(s1)
	fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:8,cap:12
	fmt.Printf("%p\n", s1)

	s1 = append(s1,9,10)
	fmt.Println(s1)
	fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:10,cap:12
	fmt.Printf("%p\n", s1)

	s1 = append(s1,11,12,13,14,15)
	fmt.Println(s1)
	fmt.Printf("len:%d,cap:%d\n", len(s1), cap(s1)) //len:15,cap:24
	fmt.Printf("%p\n", s1)
}

在这里插入图片描述
注意一点,扩容,数组是定长的,要扩容那就是开辟新的数组,旧的内存空间会被回收

在已有数组的基础上创建切片

package main

import "fmt"

func main() {
    
    
	/*
	slice := arr[start:end]
	 	切片中的数据:[start,end)
	 	arr[:end],从头到end
	 	arr[start:]从start到末尾

	 从已有的数组上,直接创建切片,该切片的底层数组就是当前的数组。
	 	长度是从start到end切割的数据量。
		但是容量从start到数组的末尾。
	  */
	a := [10]int{
    
    1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	fmt.Println("----------1.已有数组直接创建切片--------------------")
	s1 := a[:5]  //1-5
	s2 := a[3:8] //4-8
	s3 := a[5:]  // 6-10
	s4 := a[:]   // 1-10
	fmt.Println("a:", a)
	fmt.Println("s1:", s1)
	fmt.Println("s2:", s2)
	fmt.Println("s3:", s3)
	fmt.Println("s4:", s4)

	fmt.Printf("%p\n",&a)
	fmt.Printf("%p\n",s1)

	fmt.Println("----------2.长度和容量--------------------")
	fmt.Printf("s1	len:%d,cap:%d\n",len(s1),cap(s1)) //s1	len:5,cap:10
	fmt.Printf("s2	len:%d,cap:%d\n",len(s2),cap(s2)) //s2	len:5,cap:7
	fmt.Printf("s3	len:%d,cap:%d\n",len(s3),cap(s3)) //s3	len:5,cap:5
	fmt.Printf("s4	len:%d,cap:%d\n",len(s4),cap(s4)) //s4	len:10,cap:10


	fmt.Println("----------3.更改数组的内容--------------------")
	a[4] =100
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)

	fmt.Println("----------4.更改切片的内容--------------------")
	s2[2] =200
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)

	fmt.Println("----------4.更改切片的内容--------------------")
	s1 = append(s1,1,1,1,1)
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)
	fmt.Println("----------5.添加元素切片扩容--------------------")
	fmt.Println(len(s1),cap(s1))

	s1 = append(s1,2,2,2,2,2)
	fmt.Println(a)
	fmt.Println(s1)
	fmt.Println(s2)
	fmt.Println(s3)
	fmt.Println(len(s1),cap(s1))
	fmt.Printf("%p\n",s1)
	fmt.Printf("%p\n",&a)
}

切片是引用类型

package main

import "fmt"

func main() {
    
    
	/*

	按照类型来分:
		基本类型:int,float,string,bool
		复合类型:array,slice,map,struct,pointer,function,chan

	按照特点来分:
		值类型:int,float,string,bool,array
			传递的是数据副本
		引用类型:Slice
			传递的地址,多个变量指向了同一块内存地址,



	所以:切片是引用类型的数据,存储了底层数组的引用
	 */

	 //1.数组:值类型
	a1 := [4]int{
    
    1,2,3,4}
	a2 := a1  //值传递:传递的是数据
	fmt.Println(a1,a1)
	a1[0] =100
	fmt.Println(a1,a2)

	//2.切片:引用类型
	s1 :=[]int{
    
    1,2,3,4}
	s2 := s1
	fmt.Println(s1,s2)
	s1[0] =100
	fmt.Println(s1,s2)

	fmt.Printf("%p\n",s1)
	fmt.Printf("%p\n",s2)
	fmt.Printf("%p\n",&s1)
	fmt.Printf("%p\n",&s2)

}

切片自身也有地址,引用类型,所以切片中的值是个地址,指向底层数组,就是底层数组的内存地址

深拷贝和浅拷贝

package main

import "fmt"

func main() {
    
    
	/*
	深拷贝:拷贝的是数据本身。
		值类型的数据,默认都是深拷贝:array,int,float,string,bool,struct


	浅拷贝:拷贝的是数据 地址。
		导致多个变量指向同一块内存
		引用类型的数据,默认都是浅拷贝:slice,map,

		因为切片是引用类型的数据,直接拷贝的是地址。

	func copy(dst, src []Type) int
		可以实现切片的拷贝

	 */

	 s1 := []int{
    
    1,2,3,4}
	 s2 := make([]int,0) //len:0,cap:0
	 for i:=0;i<len(s1);i++{
    
    
	 	s2 = append(s2,s1[i])
	 }
	 fmt.Println(s1)
	 fmt.Println(s2)

	 s1[0] = 100
	 fmt.Println(s1)
	 fmt.Println(s2)

	 //copy() 用来给切片进行深拷贝
	 s3 := []int{
    
    7,8,9}
	 fmt.Println(s2)
	 fmt.Println(s3)

	 //copy(s2,s3) //将s3中的元素,拷贝到s2中
	 //copy(s3,s2) //将s2中的元素,拷贝到s3中
	 copy(s3[1:],s2[2:])
	 fmt.Println(s2)
	 fmt.Println(s3)

}

map

也是种容器,键值对,不是使用索引而是键

map的使用

package main

import "fmt"

func main() {
    
    
	/*
	map:映射,是一种专门用于存储键值对的集合。属于引用类型

	存储特点:
		A:存储的是无序的键值对
		B:键不能重复,并且和value值一一对应的。
				map中的key不能重复,如果重复,那么新的value会覆盖原来的,程序不会报错。

	语法结构:
		1.创建map
			var map1 map[key类型]value类型
				nil map,无法直接使用

			var map2 = make(map[key类型])value类型

			var map3 = map[key类型]value类型{
    
    key:value,key:value,key:value...}

		2.添加/修改
			map[key]=value
				如果key不存在,就是添加数据
				如果key存在,就是修改数据

		3.获取
			map[key]-->value

			value,ok := map[key]
				根据key获取对应的value
					如果key存在,value就是对应的数据,ok为true
					如果key不存在,value就是值类型的默认值,ok为false

		4.删除数据:
			delete(map,key)
				如果key存在,就可以直接删除
				如果key不存在,删除失败

		5.长度:
			len()



	每种数据类型:
		int:0
		float:0.0-->0
		string:""
		array:[00000]

		slice:nil
		map:nil

	 */

	//1.创建map
	var map1 map[int]string         //没有初始化,nil
	var map2 = make(map[int]string) //创建
	var map3 = map[string]int{
    
    "Go": 98, "Python": 87, "Java": 79, "Html": 93}
	fmt.Println(map1)
	fmt.Println(map2)
	fmt.Println(map3)

	fmt.Println(map1 == nil)
	fmt.Println(map2 == nil)
	fmt.Println(map3 == nil)
	//2.nil map
	if map1 == nil {
    
    
		map1 = make(map[int]string)
		fmt.Println(map1 == nil)
	}
	//3.存储键值对到map中
	//map1[key] = value
	map1[1] = "hello" //panic: assignment to entry in nil map
	map1[2] = "world"
	map1[3] = "memeda"
	map1[4] = "王二狗"
	map1[5] = "ruby"
	map1[6] = "三胖思密达"
	map1[7] = ""

	//4.获取数据,根据key获取对应的value值
	//根据key获取对应的value,如果key存在,获取数值,如果key不存在,获取的是value值类型的零值
	fmt.Println(map1)
	fmt.Println(map1[4]) //根据key为4,获取对应的value值
	fmt.Println(map1[40]) //""

	v1,ok :=map1[40]
	if ok {
    
    
		fmt.Println("对应的数值是:",v1)
	}else{
    
    
		fmt.Println("操作的key不存在,获取到的是零值:",v1)
	}

	//5.修改数据
	fmt.Println(map1)
	map1[3] = "如花"
	fmt.Println(map1)

	//6.删除数据
	delete(map1,3)
	fmt.Println(map1)
	delete(map1,30)
	fmt.Println(map1)

	//7.长度
	fmt.Println(len(map1))


}

map遍历

package main

import (
	"fmt"
	"sort"
)

func main() {
    
    
	/*
	map的遍历:
		使用:for range

			数组,切片:index,value
			map:key,value
	 */
	map1 := make(map[int]string)
	map1[1] = "红孩儿"
	map1[2] = "小钻风"
	map1[3] = "白骨精"
	map1[4] = "白素贞"
	map1[5] = "金角大王"
	map1[6] = "王二狗"

	//1.遍历map
	for k, v := range map1 {
    
    
		fmt.Println(k, v)
	}

	fmt.Println("----------------------")
	for i:=1;i<=len(map1);i++{
    
    
		fmt.Println(i,"--->",map1[i])
	}
	/*
	1.获取所有的key,-->切片/数组
	2.进行排序
	3.遍历key,--->map[key]
	 */
	 keys :=make([]int,0,len(map1))
	 fmt.Println(keys)

	 for k,_:=range map1{
    
    
	 	keys = append(keys,k)
	 }
	 fmt.Println(keys)

	 //冒泡排序,或者使用sort包下的排序方法
	 sort.Ints(keys)
	 fmt.Println(keys)

	 for _,key :=range keys{
    
    
	 	fmt.Println(key,map1[key])
	 }

	 s1 :=[]string{
    
    "Apple","Windows","Orange","abc","王二狗","acd","acc"}
	 fmt.Println(s1)
	 sort.Strings(s1)
	 fmt.Println(s1)
}

map结合slice

package main

import "fmt"

func main() {
    
    
	/*
	map和slice的结合使用:
		1.创建map用于存储人的信息
			name,age,sex,address

		2.每个map存储一个人的信息

		3.将这些map存入到slice中

		4.打印遍历输出
	 */

	 //1.创建map存储第一个人的信息
	 map1 := make(map[string]string)
	 map1["name"]= "王二狗"
	 map1["age"] = "30"
	 map1["sex"] = "男性"
	 map1["address"] = "北京市XX路XX号"
	 fmt.Println(map1)

	 //2.第二个人
	 map2 := make(map[string]string)
	 map2["name"] = "李小花"
	 map2["age"] = "20"
	 map2["sex"] = "女性"
	 map2["address"] = "上海市。。。"
	 fmt.Println(map2)

	 //3.
	 map3 := map[string]string{
    
    "name":"ruby","age":"30","sex":"女性","address":"杭州市"}
	 fmt.Println(map3)

	 //将map存入到slice中
	 s1 := make([]map[string]string ,0,3)
	 s1 = append(s1,map1)
	 s1 = append(s1,map2)
	 s1= append(s1,map3)

	 //遍历切片
	 for i,val :=range s1 {
    
    
	 	//val :map1,map2,map3
		fmt.Printf("第 %d 个人的信息是:\n",i+1)
		fmt.Printf("\t姓名:%s\n",val["name"])
		fmt.Printf("\t年龄:%s\n",val["age"])
		fmt.Printf("\t性别:%s\n",val["sex"])
		fmt.Printf("\t地址:%s\n",val["address"])
	 }

map是引用类型

package main

import "fmt"

func main() {
    
    
	/*
	一:数据类型:
		基本数据类型:int,float,string,bool
		复合数据类型:array,slice,map,function,pointer,struct。。。

			array:[size]数据类型
			slice:[]数据类型
			map:map[key的类型]value的类型


	二:存储特点:
		值类型:int,float,string,bool,array,struct
		引用类型:slice,map

			make(),slice,map,chan


	 */

	 map1 := make(map[int]string)
	 map2 :=make(map[string]float64)
	 fmt.Printf("%T\n",map1)
	 fmt.Printf("%T\n",map2)

	 map3 := make(map[string]map[string]string) //map[string]map[string]string
	 m1 :=make(map[string]string)
	 m1["name"] = "王二狗"
	 m1["age"] = "30"
	 m1["salary"] = "3000"
	 map3["hr"] = m1

	 m2 :=make(map[string]string)
	 m2["name"]="ruby"
	 m2["age"]="28"
	 m2["salary"]="8000"

	 map3["总经理"]= m2
	 fmt.Println(map3)

	 fmt.Println("---------------")

	 map4 := make(map[string]string)
	 map4["王二狗"] = "矮矬穷"
	 map4["李小花"] = "白富美"
	 map4["ruby"] = "住在隔壁"

	 fmt.Println(map4)

	 map5 := map4
	 fmt.Println(map5)

	 map5["王二狗"]= "高富帅"
	 fmt.Println(map4)
	 fmt.Println(map5)

}

string

string的使用

package main

import "fmt"

func main() {
    
    
	/*
	Go中的字符串是一个字节的切片。
		可以通过将其内容封装在“”中来创建字符串。Go中的字符串是Unicode兼容的,并且是UTF-8编码的。

	字符串是一些字节的集合。
		理解为一个字符的序列。
		每个字符都有固定的位置(索引,下标,index:从0开始,到长度减1)

	语法:"",``
		""
		"a","b","中"
		"abc","hello"
	字符:--->对应编码表中的编码值
		A-->65
		B-->66
		a-->97
		...

	字节:byte-->uint8
		utf8
	 */
	 //1.定义字符串
	 s1 := "hello中国"
	 s2 := `hello world`
	 fmt.Println(s1)
	 fmt.Println(s2)

	 //2.字符串的长度:返回的是字节的个数
	 fmt.Println(len(s1))
	 fmt.Println(len(s2))

	 //3.获取某个字节
	fmt.Println(s2[0])//获取字符串中的第一个字节
	a := 'h'
	b := 104
	fmt.Printf("%c,%c,%c\n",s2[0],a,b)

	//4.字符串的遍历
	for i:=0;i<len(s2);i++{
    
    
		//fmt.Println(s2[i])
		fmt.Printf("%c\t",s2[i])
	}
	fmt.Println()

	//for range
	for _,v := range s2{
    
    
		//fmt.Println(i,v)
		fmt.Printf("%c",v)
	}
	fmt.Println()


	//5.字符串是字节的集合
	slice1 := []byte{
    
    65,66,67,68,69}
	s3 := string(slice1) //根据一个字节切片,构建字符串
	fmt.Println(s3)

	s4 := "abcdef"
	slice2 := []byte(s4) //根据字符串,获取对应的字节切片
	fmt.Println(slice2)

	//6.字符串不能修改
	fmt.Println(s4)
	//s4[2] = 'B'
}

strings包的使用

package main

import (
	"fmt"
	"strings"
)

func main() {
    
    
	/*
	strings包下的关于字符串的函数

	 */
	s1 := "helloworld"
	//1.是否包含指定的内容-->bool
	fmt.Println(strings.Contains(s1, "abc"))
	//2.是否包含chars中任意的一个字符即可
	fmt.Println(strings.ContainsAny(s1, "abcd"))
	//3.统计substr在s中出现的次数
	fmt.Println(strings.Count(s1, "lloo"))

	//4.以xxx前缀开头,以xxx后缀结尾
	s2 := "20190525课堂笔记.txt"
	if strings.HasPrefix(s2, "201905") {
    
    
		fmt.Println("19年5月的文件。。")
	}
	if strings.HasSuffix(s2, ".txt") {
    
    
		fmt.Println("文本文档。。")
	}

	//索引
	//helloworld
	fmt.Println(strings.Index(s1, "lloo"))     //查找substr在s中的位置,如果不存在就返回-1
	fmt.Println(strings.IndexAny(s1, "abcdh")) //查找chars中任意的一个字符,出现在s中的位置
	fmt.Println(strings.LastIndex(s1, "l"))    //查找substr在s中最后一次出现的位置

	//字符串的拼接
	ss1 := []string{
    
    "abc","world","hello","ruby"}
	s3 := strings.Join(ss1,"-")
	fmt.Println(s3)

	//切割
	s4 := "123,4563,aaa,49595,45"
	ss2 := strings.Split(s4,",")
	//fmt.Println(ss2)
	for i := 0;i<len(ss2);i++{
    
    
		fmt.Println(ss2[i])
	}

	//重复,自己拼接自己count次
	s5 :=strings.Repeat("hello",5)
	fmt.Println(s5)

	//替换
	//helloworld
	s6 := strings.Replace(s1,"l","*",-1)
	fmt.Println(s6)
	//fmt.Println(strings.Repeat("hello",5))

	s7:="heLLo WOrlD**123.."
	fmt.Println(strings.ToLower(s7))
	fmt.Println(strings.ToUpper(s7))

	/*
	截取子串:
	substring(start,end)-->substr
	str[start:end]-->substr
		包含start,不包含end下标
	 */
	 fmt.Println(s1)
	 s8 := s1[:5]
	 fmt.Println(s8)
	 fmt.Println(s1[5:])
}

strconv的使用

package main

import (
	"fmt"
	"strconv"
)

func main() {
    
    
	/*
	strconv包:字符串和基本类型之前的转换
		string convert
	 */
	//fmt.Println("aa"+100)
	//1.bool类型
	s1 := "true"
	b1,err := strconv.ParseBool(s1)
	if err != nil{
    
    
		fmt.Println(err)
		return
	}
	fmt.Printf("%T,%t\n",b1,b1)

	ss1 := strconv.FormatBool(b1)
	fmt.Printf("%T,%s\n",ss1,ss1)

	//2.整数
	s2 := "100"
	i2,err := strconv.ParseInt(s2,2,64)
	if err != nil{
    
    
		fmt.Println(err)
		return
	}
	fmt.Printf("%T,%d\n",i2,i2)

	ss2 := strconv.FormatInt(i2,10)
	fmt.Printf("%T,%s\n",ss2,ss2)

	//itoa(),atoi()
	i3,err := strconv.Atoi("-42") //转为int类型
	fmt.Printf("%T,%d\n",i3,i3)
	ss3:=strconv.Itoa(-42)
	fmt.Printf("%T,%s\n",ss3,ss3)
}

函数

函数介绍

自定义函数位置没有确切要求
定义函数名称,跟c一样,数字字母下划线,其中数字不能开头,大写开头的函数表示可以跨包使用,小写的表示在本包里使用

package main

import "fmt"

func main() {
    
     //程序的入口,是一个特殊的函数,程序执行时自动调用
	/*
	函数:function
	一、概念:
		具有特定功能的代码,可以被多次调用执行。
	二、意义:
		1.可以避免重复的代码
		2.增强程序的扩展性
	三、使用
		step1:函数的定义,也叫声明
		step2:函数的调用,就是执行函数中代码的过程
	四、语法
		1.定义函数的语法
			func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) {
    
    
				//这里是处理逻辑代码
				//返回多个值
				return value1, value2
			}
			A:func,定义函数的关键字
			B:funcName,函数的名字
			C:(),函数的标志
			D:参数列表:形式参数用于接收外部传入函数中的数据
			E:返回值列表:函数执行后返回给调用处的结果

		2、调用函数的语法
			函数名(实际参数)

		函数的调用处,就是函数调用的位置

		3、注意事项
			A:函数必须先定义,再调用,如果不定义:undefined: getSum
				定义了函数,没有调用,那么函数就失去了意义

			B:函数名不能冲突
			C:main(),是一个特殊的函数,作为程序的入口,由系统自动调用
				而其他函数,程序中通过函数名来调用。
	 */
	//求1-10的和
	getSum() //函数的调用处

	fmt.Println("hello world....")

	//求1-10的和
	getSum()

	fmt.Println("aaa...")

	//求1-10的和
	getSum()
}

//定义一个函数:用于求1-10的和
func getSum() {
    
    
	sum := 0
	for i := 1; i <= 100; i++ {
    
    
		sum += i
	}
	fmt.Printf("1-100的和是:%d\n", sum)
}

同样函数中的变量也有自己的作用域,函数的变量等仅作用域函数块,函数调用完则变量的内存空间销毁

函数要先定义才能调用,至于定于函数的代码块在main函数前后无关

函数的参数

package main

import "fmt"

func main() {
    
    
	/*
	函数的参数:
		形式参数:也叫形参。函数定义的时候,用于接收外部传入的数据的变量。
			函数中,某些变量的数值无法确定,需要由外部传入数据。

		实际参数:也叫实参。函数调用的时候,给形参赋值的实际的数据


	函数调用:
		1.函数名:声明的函数名和调用的函数名要统一
		2.实参必须严格匹配形参:顺序,个数,类型,一一对应的。
	 */

	//1.求1-10的和
	getSum(10)
	//2.求1-20的和
	getSum(20)
	fmt.Println()

	//3.1-100的和
	getSum(100)

	//4.求2个整数的和
	getAdd(20,10)
	getAdd2(1,2)

	fun1(1.3,2.4,"hello")
}

//定义一个函数:用于求1-10的和
func getSum(n int) {
    
    
	sum := 0
	for i := 1; i <= n; i++ {
    
    
		sum += i
	}
	fmt.Printf("1-%d的和是:%d\n",n, sum)
}

func getAdd(a int,b int){
    
    
	sum := a + b
	fmt.Printf("%d + %d = %d \n",a,b,sum)
}

func getAdd2(a, b int){
    
    //参数的类型一致,可以简写在一起
	fmt.Printf("a:%d,b:%d\n",a,b)
}

func fun1(a,b float64,c string){
    
    
	fmt.Printf("a:%.2f,b:%.2f,c:%s\n",a,b,c)
}

调用函数时,会为形参开辟内存空间

函数调用完成,内存空间就被销毁了,回到函数的调用处,继续运行代码

形参赋值时即实参对应形参有先后顺序,一一对应,且类型也要对应

函数的可变参

package main

import "fmt"

func main() {
    
    
	/*
		可变参数:
			概念:一个函数的参数的类型确定,但是个数不确定,就可以使用可变参数。

			语法:
				参数名 ... 参数的类型

				对于函数,可变参数相当于一个切片。
				调用函数的时候,可以传入0-多个参数。

				Println(),Printf(),Print()
				append()

			注意事项:
				A:如果一个函数的参数是可变参数,同时还有其他的参数,可变参数要放在
					参数列表的最后。
				B:一个函数的参数列表中最多只能有一个可变参数。

	 */
	 //1.求和
	 getSum()

	 getSum(1,2,3,4,5)

	 getSum(1,2,3,4,5,6,7,8,9,10)

	 //2.切片
	 s1 :=[]int{
    
    1,2,3,4,5}
	 getSum(s1...)
}

func getSum(nums ... int){
    
    
	//fmt.Printf("%T\n",nums) //[]int,可变参是切片
	sum := 0
	for i := 0;i <len(nums);i++{
    
    
		sum += nums[i]
	}
	fmt.Println("总和是:",sum)
}

func fun1(s1,s2 string,nums ... float64){
    
    

}

参数传递

package main

import "fmt"

func main() {
    
    
	/*
	数据类型:
		一:按照数据类型来分:
				基本数据类型:
					int,float,string,bool
				复合数据类型:
					array,slice,map,struct,interface。。。。

		二:按照数据的存储特点来分:
				值类型的数据:操作的是数值本身。
					int,float64,bool,string,array
				引用类型的数据:操作的是数据的地址
					slice,map,chan

	参数传递:
		A:值传递:传递的是数据的副本。修改数据,对于原始的数据没有影响的。
			值类型的数据,默认都是值传递:基础类型,array,struct



		B:引用传递:传递的是数据的地址。导致多个变量指向同一块内存。
			引用类型的数据,默认都是引用传递:slice,map,chan

	 */
	 arr1 := [4]int{
    
    1,2,3,4}
	 fmt.Println("函数调用前,数组的数据:",arr1) //[1 2 3 4]
	 fun1(arr1)
	 fmt.Println("函数调用后,数组的数据:",arr1) //[1 2 3 4]

	 fmt.Println("---------------------------------")

	 s1 :=[] int{
    
    1,2,3,4}
	 fmt.Println("函数调用前,切片的数据:",s1) // [1 2 3 4]
	 fun2(s1)
	 fmt.Println("函数调用后,切片刀数据:",s1) //[100 2 3 4]
}

func fun2(s2 []int){
    
       //引用传递,s1和s2指向同一个底层数组
	fmt.Println("函数中,切片的数据:",s2) //[1 2 3 4]
	s2[0] = 100
	fmt.Println("函数中,切片的数据更改后:",s2) //[100 2 3 4]
}
func fun1(arr2 [4]int){
    
       //数组是值类型,传递的时候是值传递,其实就是拷贝arr1的数据一份到arr2,两个数组占用的是不同的内存空间
	fmt.Println("函数中,数组的数据:",arr2) //[1 2 3 4]
	arr2[0] =100
	fmt.Println("函数中,数组的数据修改后:",arr2) //[100 2 3 4]

函数的返回值

package main

import "fmt"

func main() {
    
    
	/*
	函数的返回值:
		一个函数的执行结果,返回给函数的调用处。执行结果就叫做函数的返回值。

	return语句:
		一个函数的定义上有返回值,那么函数中必须使用return语句,将结果返回给调用处。
		函数返回的结果,必须和函数定义的一致:类型,个数,顺序。

		1.将函数的结果返回给调用处
		2.同时结束了该函数的执行

	空白标识符,专门用于舍弃数据:_
	 */

	//1.设计一个函数,用于求1-10的和,将结果在主函数中打印输出
	res := getSum()   //会先为变量res开辟内存空间,值还未知,需要调用函数
	fmt.Println("1-10的和:",res)

	fmt.Println(getSum2()) //5050

	res1,res2 := rectangle(5,3)
	fmt.Println("周长:",res1,",面积:",res2)
	res3,res4 := rectangle2(5,3)   //按顺序来
	fmt.Println("周长:",res3,",面积:",res4)

	_,res5 := rectangle(5,3)  //_表示舍弃
	fmt.Println(res5)
}

//函数,用于求矩形的周长和面积
func rectangle(len,wid float64)(float64,float64){
    
    
	perimeter := (len +wid) *2
	area := len * wid
	return perimeter,area  //多返回值
}

func rectangle2(len,wid float64)(peri float64,area float64){
    
       //提前写好返回值列表
	peri = (len +wid)*2
	area = len*wid
	return
}


func fun1()(float64,float64,string){
    
    
	return 2.4,5.6,"hello"
}


//定义一个函数,带返回值
func getSum()int {
    
     //返回值的类型,这里没有定义返回值的名称
	sum := 0
	for i:=1;i<=10;i++{
    
    
		sum += i
	}
	return sum //返回sum

}

func getSum2()(sum int){
    
    //定义函数时,指明要返回的数据是哪一个,返回值的名称和类型
	for i:=1;i<=100;i++{
    
    
		sum += i
	}
	return //直接return即可,也可以写成return 1,这相当于将sum变量重新赋值,sum=1
}

return语句

package main

import "fmt"

func main() {
    
    
	/*
	return语句:词义"返回"
		A:一个函数有返回值,那么使用return将返回值返回给调用处
		B:同时意味着结束了函数的执行

	注意点:
		1.一个函数定义了返回值,必须使用return语句将结果返回给调用处。return后的数据必须和函数定义的一致:个数,类型,顺序。
		2.可以使用_,来舍弃多余的返回值
		3.如果一个函数定义了有返回值,那么函数中有分支,循环,那么要保证,无论执行了哪个分支,都要有return语句被执行到
		4.如果一个函数没有定义返回值,那么函数中也可以使用return,专门用于结束函数的执行。。
	 */
	a, b, c := fun1()
	fmt.Println(a, b, c)
	_, _, d := fun1()
	fmt.Println(d)
	fmt.Println(fun2(-30))
	fun3()
}

func fun1() (int, float64, string) {
    
    
	return 0, 0, "hello"
}

func fun2(age int) int {
    
    
	if age >= 0 {
    
    
		return age

	} else {
    
    
		fmt.Println("年龄不能为负。。")
		return 0
	}
}

func fun3(){
    
    
	for i:=0;i<10;i++{
    
    
		if i == 5{
    
    
			//break //?强制结束循环
			return
		}
		fmt.Println(i)
	}
	fmt.Println("fun3()...over....")
}

函数中的变量的作用域

Ctrl+b跳转到定义处
主要通过代码块来识别作用域

package main

import "fmt"

//全局变量的定义
//num3 := 1000//不支持简短定义的写法
var num3 = 1000   //如果是大写,还能跨包调用

func main() {
    
    
	/*
		作用域:变量可以使用的范围。
			局部变量:函数内部定义的变量,就叫做局部变量。
						变量在哪里定义,就只能在哪个范围使用,超出这个范围,我们认为变量就被销毁了。

			全局变量:函数外部定义的变量,就叫做全局变量。
						所有的函数都可以使用,而且共享这一份数据

	*/
	//定义在main函数中,所以n的作用域就是main函数的范围内
	n := 10
	fmt.Println(n)

	if a := 1; a <= 10 {
    
    
		fmt.Println(a) // 1
		fmt.Println(n) // 10
	}
	//fmt.Println(a) //不能访问a,出了作用域
	fmt.Println(n)

	if b := 1; b <= 10 {
    
    
		n := 20
		fmt.Println(b) // 1
		fmt.Println(n) // 20
	}

	fun1()
	fun2()
	fmt.Println("main中访问全局变量:", num3) //2000

}

func fun1() {
    
    
	//fmt.Println(n)
	num1 := 100
	fmt.Println("fun1()函数中:num1:", num1)
	num3 = 2000
	fmt.Println("fun1()函数,访问全局变量:", num3) // 2000
}

func fun2() {
    
    
	num1 := 200
	fmt.Println(num1)
	fmt.Println("fun2()函数,访问全局变量:", num3) //2000
}

递归函数

就是自己调用自己,代码量少

package main

import "fmt"

func main() {
    
    
	/*
		递归函数(recursion):一个函数自己调用自己,就叫做递归函数。
			递归函数要有一个出口,逐渐的向出口靠近
 */
	//1.求1-5的和
	sum := getSum(5)
	fmt.Println(sum)

	//2.fibonacci数列:
	/*
	n<=2是为1,n>2,f(n)=f(n-1)+f(n-2)
	1	2	3	4	5	6	7	8	9	10	11	12		。。。
	1	1	2	3	5	8	13	21	34	55	89	144


	 */
	 res := getFibonacci(12)
	 fmt.Println(res)
}
func getFibonacci(n int)int{
    
    
	if n== 1 || n == 2{
    
    
		return 1
	}
	return getFibonacci(n-1)+getFibonacci(n-2)
}

func getSum(n int)int{
    
    
	fmt.Println("**********")
	if n == 1{
    
    
		return 1
	}
	return getSum(n-1) + n
}
/*
求1-5的和
getSum(5)

	getSum(4) + 5

		getSum(3) + 4

			getSum(2) + 3

				getSum(1) + 2

				1
 */

这里用递归来实现,用循环也是可以的,但是用递归代码量会少些
递归是自己调用自己,所以得设置个出口,比如上面的return

用循环实现其实思维也像递归,就是自己调用自己,比如sum=sum+i(sum+=i),i拿来循环

不习惯的可以用for循环来实现,for循环可以实现很多简单的算法思维

defer语句

package main

import "fmt"

func main() {
    
    //外围函数
	/*
	defer的词义:"延迟","推迟"
	在go语言中,使用defer关键字来延迟一个函数或者方法的执行。

	1.defer函数或方法:一个函数或方法的执行被延迟了。

	2.defer的用法:
		A:对象.close(),临时文件的删除。。。
				文件.open()
				defer close()
				读或写

		B:go语言中关于异常的处理,使用panic()和recover()
			panic函数用于引发恐慌,导致程序中断执行
			recover函数用于恢复程序的执行,recover()语法上要求必须在defer中执行。


	3.如果多个defer函数:先延迟的后执行,后延迟的先执行。

	4.defer函数传递参数的时候:defer函数调用时,就已经传递了参数数据了,只是暂时不执行函数中的代码而已。

	5.defer函数注意点:
		defer函数:
当外围函数中的语句正常执行完毕时,只有其中所有的延迟函数都执行完毕,外围函数才会真正的结束执行。
当执行外围函数中的return语句时,只有其中所有的延迟函数都执行完毕后,外围函数才会真正返回。
当外围函数中的代码引发运行恐慌时,只有其中所有的延迟函数都执行完毕后,该运行时恐慌才会真正被扩展至调用函数。

	 */
	 //defer fun1("hello") //也被延迟了,多个推迟语句是就设计到栈last in first out,每条defer依据都会一次压入栈中,拿出来的时候就是从栈顶拿,所以defer语句是逆序执行的
	 //fmt.Println("12345")
	 //defer fun1("world") //被延迟了,当当前的函数(main)函数执行完后再执行
	 //fmt.Println("王二狗")

	 a := 2
	 fmt.Println(a) //2
	 defer fun2(a)   //defer函数调用时就已经传递参数了,所以a是2,只是暂时不执行函数的代码而已
	 a++
	 fmt.Println("main中:",a)//3

	 fmt.Println(fun3())
}

func fun3()int{
    
    
	fmt.Println("fun3()函数的执行。。。")
	defer fun1("haha")
	return 0   //外围函数要return,前提是defer语句执行完了
}

func fun2(a int){
    
    // a = 2
	fmt.Println("fun2()函数中打印a:",a) // 2
}
func fun1(s string){
    
    
	fmt.Println(s)
}

函数的数据类型

package main

import "fmt"

func main() {
    
    
	/*
	go语言的数据类型:
		基本数据类型:
				int,float,bool,string

		复合数据类型:
				array,slice,map,function,pointer,struct,interface。。。


	函数的类型:
			func(参数列表的数据类型)(返回值列表的数据类型)

	 */

	a := 10
	fmt.Printf("%T\n", a) //int
	b := [4]int{
    
    1, 2, 3, 4}
	fmt.Printf("%T\n", b) //[4]int
	/*
	[4]string
	[6]float64
	 */
	c := []int{
    
    1, 2, 3, 4}
	fmt.Printf("%T\n", c) //[]int

	d := make(map[int]string)
	fmt.Printf("%T\n", d)
	/*
	map[string]string
	map[string]map[int]string   key是string,value是map
	 */

	fmt.Printf("%T\n", fun1) //func()表示函数类型,fun1表示函数自身
	fmt.Printf("%T\n", fun2) //func(int) int
	fmt.Printf("%T\n",fun3) //func(float64, int, int) (int, int)
	fmt.Printf("%T\n",fun4) //func(string,string,int,int)(string,int ,float64)
}

func fun1() {
    
    }

func fun2(a int) int {
    
    
	return 0
}

func fun3(a float64, b, c int) (int, int) {
    
    
	return 0, 0
}

func fun4(a,b string,c,d int)(string,int ,float64){
    
    
	return "",0,0
}

函数的本质

package main

import "fmt"

func main() {
    
    
	/*
		Go语言的数据类型:

			数值类型:整数,浮点
				进行运算操作,加减乘除,打印
			字符串:
				可以获取单个字符,截取子串,遍历,strings包下的函数操作。。
			数组,切片,map。。
				存储数据,修改数据,获取数据,遍历数据。。。
			函数:
				加(),进行调用

			注意点:
				函数作为一种复合数据类型,可以看做是一种特殊的变量。
					函数名():将函数进行调用,函数中的代码会全部执行,然后将return的结果返回给调用处
					函数名:指向函数体的内存地址

	*/
	//1.整型
	a := 10
	//运算:
	a += 5
	fmt.Println("a:", a)
	//2.数组,切片,map。。容器
	b := [4]int{
    
    1, 2, 3, 4}
	b[0] = 100
	for i := 0; i < len(b); i++ {
    
    
		fmt.Printf("%d\t", b[i])
	}
	fmt.Println()

	//3.函数做一个变量
	fmt.Printf("%T\n", fun1) //func(int, int)
	fmt.Println(fun1)        // fun1() fun1  0x1094db0,看做函数名对应的函数体的地址

	//4.直接定义一个函数类型的变量
	var c func(int, int)
	fmt.Println(c) //<nil> 空

	//var d  string
	//d = "hello"
	c = fun1 //将fun1的值(函数体的地址)赋值给c
	fmt.Println(c)

	fun1(10, 20)
	c(100, 200) //c也是函数类型的,加小括号也可以被调用

	res1 := fun2       //将fun2的值(函数的地址)赋值给res1,res1和fun2指向同一个函数体
	res2 := fun2(1, 2) //将fun2函数进行调用,将函数的执行结果赋值给res2,相当于:a+b
	fmt.Println(res1)
	fmt.Println(res2)

	fmt.Println(res1(10, 20)) //也可以被调用
	//res2() //cannot call non-function res2 (type int)
}
func fun2(a, b int) int {
    
    
	return a + b
}
func fun1(a, b int) {
    
    
	fmt.Printf("a:%d,b:%d\n", a, b)
}

函数名()是调用的
函数名是一种特殊的标量,其实也是一种指针类型,只是函数的内存地址,所以赋值时值传递,传递了地址,但是打印类型时%T不是直接显示指针,而是函数名(参数列表)(返回值列表)

匿名函数

package main

import "fmt"

func main() {
    
    
	/*
	匿名:没有名字
		匿名函数:没有名字的函数。

	定义一个匿名函数,直接进行调用。通常只能使用一次。也可以使用匿名函数赋值给某个函数变量,那么就可以调用多次了。

	匿名函数:
		Go语言是支持函数式编程:
		1.将匿名函数作为另一个函数的参数,回调函数
		2.将匿名函数作为另一个函数的返回值,可以形成闭包结构。
	 */
	 fun1()
	 fun1()
	 fun2 := fun1
	 fun2()

	 //匿名函数
	 func (){
    
    
	 	fmt.Println("我是一个匿名函数。。")
	 }()

	 fun3:=func(){
    
    
	 	fmt.Println("我也是一个匿名函数。。")
	 }
	 fun3()
	 fun3()

	 //定义带参数的匿名函数
	 func (a,b int){
    
    
	 	fmt.Println(a,b)
	 }(1,2)

	 //定义带返回值的匿名函数
	 res1 := func (a, b int)int{
    
    
	 	return a + b
	 }(10,20) //匿名函数调用了,将执行结果给res1
	 fmt.Println(res1)

	 res2 := func (a,b int)int{
    
    
	 	return a + b
	 } //将匿名函数的值,赋值给res2
	 fmt.Println(res2)

	 fmt.Println(res2(100,200))
}

func fun1(){
    
    
	fmt.Println("我是fun1()函数。。")
}

回调函数

函数式编程,高阶函数接收函数式参数

package main

import "fmt"

func main() {
    
    
	/*
	高阶函数:
		根据go语言的数据类型的特点,可以将一个函数作为另一个函数的参数。

	fun1(),fun2()
	将fun1函数作为了fun2这个函数的参数。

			fun2函数:就叫高阶函数
				接收了一个函数作为参数的函数,高阶函数

			fun1函数:回调函数
				作为另一个函数的参数的函数,叫做回调函数。
	 */
	//设计一个函数,用于求两个整数的加减乘除运算
	fmt.Printf("%T\n", add)  //func(int, int) int
	fmt.Printf("%T\n", oper) //func(int, int, func(int, int) int) int

	res1 := add(1, 2)
	fmt.Println(res1)

	res2 := oper(10, 20, add)
	fmt.Println(res2)

	res3 := oper(5,2,sub)
	fmt.Println(res3)

	fun1:=func(a,b int)int{
    
    
		return a * b
	}

	res4:=oper(10,4,fun1)
	fmt.Println(res4)


	res5 := oper(100,8,func(a,b int)int{
    
    
		if b == 0{
    
    
			fmt.Println("除数不能为零")
			return 0
		}
		return a / b
	})
	fmt.Println(res5)

}
func oper(a, b int, fun func(int, int) int) int {
    
    
	fmt.Println(a, b, fun) //打印3个参数
	res := fun(a, b)
	return res
}

//加法运算
func add(a, b int) int {
    
    
	return a + b
}

//减法
func sub(a, b int) int {
    
    
	return a - b
}

闭包

首先要区分开函数的定义和调用
只有函数被调用时,它的代码才会执行

package main

import "fmt"

func main() {
    
    
	/*
	go语言支持函数式编程:
		支持将一个函数作为另一个函数的参数,
		也支持将一个函数作为另一个函数的返回值。

	闭包(closure):
		一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量(外层函数中的参数,或者外层函数中直接定义的变量),并且该外层函数的返回值就是这个内层函数。

		这个内层函数和外层函数的局部变量,统称为闭包结构。


		局部变量的生命周期会发生改变,正常的局部变量随着函数调用而创建,随着函数的结束而销毁。
		但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还要继续使用。


	 */
	 res1 := increment() //res1 = fun
	 fmt.Printf("%T\n",res1) //func() int
	 fmt.Println(res1)
	 v1 := res1()
	 fmt.Println(v1) //1
	 v2 := res1()
	 fmt.Println(v2) //2
	 fmt.Println(res1())
	 fmt.Println(res1())
	 fmt.Println(res1())
	 fmt.Println(res1())

	 res2 := increment()
	 fmt.Println(res2)
	 v3 :=res2()
	 fmt.Println(v3) //1
	 fmt.Println(res2())

	 fmt.Println(res1())
}

func increment()func()int{
    
     //外层函数
	//1.定义了一个局部变量
	i := 0   //注意这里,按常理,这里的i是函数中定义的,是随着函数的调用而创建的局部变量,那么函数调用完之后i的内存地址就应该被销毁了,但这里不是,如果函数的返回值是内层函数,而且内层函数有设计到这个变量i,那么这个变量i不会随着函数的而销毁(函数传进来的实参也一样)
	//2.定义了一个匿名函数,给变量自增并返回
	fun := func ()int{
    
     //内层函数
		i++
		return i
	}
	//3.返回该匿名函数
	return fun
}

注意一点这里每次调用函数,都会有新的i,然后内层函数设计的自然也是新的外层函数的变量

指针

指针基础

指针你可以将它直接理解成内存地址,因为它的值就是内存地址

指针是种特殊变量,存放着另外的变量的地址

内存地址是有go来分配的,所以重新run后变量的内存地址可能会发生变化

package main

import "fmt"

func main() {
    
    
	/*
	指针:pointer
		存储了另一个变量的内存地址的变量。

	 */
	 //1.定义一个int类型的变量
	 a := 10
	 fmt.Println("a的数值是:",a) //10
	 fmt.Printf("%T\n",a) //int
	 fmt.Printf("a的地址是:%p\n",&a) //0xc00008a008

	 //2.创建一个指针变量,用于存储变量a的地址
	 var p1 *int
	 fmt.Println(p1) //<nil>,空指针
	 p1 = &a //p1指向了a的内存地址
	 fmt.Println("p1的数值:",p1) //p1中存储的是a的地址
	 fmt.Printf("p1自己的地址:%p\n",&p1)//&取地址符
	 fmt.Println("p1的数值,是a的地址,该地址存储的数据:",*p1)//获取指针指向的变量的数值

	 //3.操作变量,更改数值 ,并不会改变地址,改变内存地址存储的数据
	 a = 100
	 fmt.Println(a)
	 fmt.Printf("%p\n",&a)

	 //4.通过指针,改变变量的数值
	 *p1 = 200
	 fmt.Println(a)

	 //5.指针的指针
	 var p2 **int
	 fmt.Println(p2)
	 p2 = &p1
	 fmt.Printf("%T,%T,%T\n",a,p1,p2) //int, *int, **int
	 fmt.Println("p2的数值:",p2)   //p1的地址
	 fmt.Printf("p2自己的地址:%p\n",&p2)
	 fmt.Println("p2中存储的地址,对应的数值,就是p1的地址,对应的数据:",*p2)
	 fmt.Println("p2中存储的地址,对应的数值,再获取对应的数值:",**p2)

}

普通的变量,通过变量名就能找到内存地址

数组指针和指针数组

package main

import "fmt"

func main() {
    
    
	/*
	数组指针:首先是一个指针,一个数组的地址。
		*[4]Type

	指针数组:首先是一个数组,存储的数据类型是指针,即存储的是地址
		[4]*Type


		*[5]float64,指针,一个存储了5个浮点类型数据的数组的指针
		*[3]string,指针,数组的指针,存储了3个字符串
		[3]*string,数组,存储了3个字符串的指针地址的数组
		[5]*float64,数组,存储了5个浮点数据的地址的数组
		*[5]*float64,指针,一个数组的指针,存储了5个float类型的数据的指针地址的数组的指针
		*[3]*string,指针,存储了3个字符串的指针地址的数组的指针
		**[4]string,指针,存储了4个字符串数据的数组的指针的指针
		**[4]*string,指针,存储了4个字符串的指针地址的数组,的指针的指针

判断很简单,*开头就是指针,[num]开头就是数组
	 */
	 //1.创建一个普通的数组
	 arr1 :=[4]int{
    
    1,2,3,4}
	 fmt.Println(arr1)

	 //2.创建一个指针,存储该数组的地址--->数组指针
	 var p1 *[4]int
	 p1 = &arr1
	 fmt.Println(p1) //&[1 2 3 4]
	 fmt.Printf("%p\n",p1) //数组arr1的地址
	 fmt.Printf("%p\n",&p1) //p1指针自己的地址

	 //3.根据数组指针,操作数组
	(*p1)[0] = 100
	fmt.Println(arr1)

	p1[0] = 200 //简化写法
	fmt.Println(arr1)

	//4.指针数组
	a := 1
	b := 2
	c := 3
	d := 4
	arr2 :=[4]int{
    
    a,b,c,d}
	arr3 :=[4]*int{
    
    &a,&b,&c,&d}
	fmt.Println(arr2) //[1 2 3 4]
	fmt.Println(arr3)
	arr2[0] =100
	fmt.Println(arr2)
	fmt.Println(a)
	*arr3[0] = 200
	fmt.Println(arr3)
	fmt.Println(a)

	b = 1000
	fmt.Println(arr2)
	fmt.Println(arr3)
	for i:=0;i<len(arr3);i++{
    
    
		fmt.Println(*arr3[i])
	}
}

函数指针和指针函数

函数自身其实就是个指针

package main

import "fmt"

func main() {
    
    
	/*
	函数指针:一个指针,指向了一个函数的指针。
		因为go语言中,function,默认看作一个指针,没有*。


		slice,map,function

	指针函数:一个函数,该函数的返回值是一个指针。

	 */
	var a func()
	a = fun1   //fun1数据其实是地址,这里就是引用传递,传递的是地址
	a()

	arr1 := fun2()
	fmt.Printf("arr1的类型:%T,地址:%p,数值:%v\n",arr1,&arr1,arr1)

	arr2 := fun3()
	fmt.Printf("arr2的类型:%T,地址:%p,数值:%v\n",arr2,&arr2,arr2)
	fmt.Printf("arr2指针中存储的数组的地址:%p\n",arr2)
}
func fun3()*[4]int{
    
    
	arr := [4]int{
    
    5,6,7,8}
	fmt.Printf("函数中arr的地址:%p\n",&arr)
	return &arr
}

func fun2()[4]int{
    
    //普通函数
	arr :=[4]int{
    
    1,2,3,4}
	return arr
}


func fun1(){
    
    
	fmt.Println("fun1().....")
}

指针作为函数的参数

package main

import "fmt"

func main() {
    
    
	/*
	指针作为参数:

	参数的传递:值传递,引用传递
	 */
	 a := 10
	 fmt.Println("fun1()函数调用前,a:",a)
	 fun1(a)
	 fmt.Println("fun1()函数调用后,a:",a)

	 fun2(&a)   //指针作为参数,那么就传递地址
	 fmt.Println("fun2()函数调用后,a:",a)

	 arr1 := [4]int{
    
    1,2,3,4}
	 fmt.Println("fun3()函数调用前:",arr1)
	 fun3(arr1)
	 fmt.Println("fun3()函数调用后:",arr1)

	 fun4(&arr1)
	 fmt.Println("fun4()函数调用后:",arr1)

	 s1 := []int{
    
    1,2,3,4,5}
}
func fun4(p2 *[4]int){
    
     // 引用传递
	fmt.Println("fun4()函数中的数组指针:",p2)
	p2[0] = 200
	fmt.Println("fun4()函数中的数组指针:",p2)
}

func fun3(arr2 [4]int){
    
     // 值传递
	fmt.Println("fun3()函数中数组的:",arr2)
	arr2[0] = 100
	fmt.Println("fun3()函数中修改数组:",arr2)
}




func fun1(num int){
    
     // 值传递:num = a = 10
	fmt.Println("fun1()函数中,num的值:",num)
	num = 100
	fmt.Println("fun1()函数中修改num:",num)
}

func fun2(p1 *int){
    
     //传递的是a的地址,就是引用传递,p1=&a
	fmt.Println("fun2()函数中,p1:",*p1)
	*p1 = 200   //函数调用完成后,参数也会销毁了,但是是通过地址更改的,所以main函数中的局部环境变量会修改了
	fmt.Println("fun2()函数中,修改p1:",*p1)
}

对于切片就没必要设置指针了,它本身就是个指针,存放的就是地址,是引用了类型,一般是对值类型设置指针,会节省资源,值类型在传递的时候是深拷贝,会赋值一份,浪费资源

结构体

结构体基础

结构体的成员如果首字母大写,表示公有的,其他人导包后也可以调用

package main

import "fmt"

func main() {
    
    
	/*
	结构体:是由一系列具有相同类型或不同类型的数据构成的数据集合
		结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”
	 */
	 //1.方法一
	 var p1 Person
	 fmt.Println(p1)
	 p1.name = "王二狗"
	 p1.age = 30
	 p1.sex = "男"
	 p1.address = "北京市"
	 fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p1.name,p1.age,p1.sex,p1.address)

	//2.方法二
	p2 := Person{
    
    }
	p2.name = "Ruby"
	p2.age = 28
	p2.sex= "女"
	p2.address = "上海市"
	fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p2.name,p2.age,p2.sex,p2.address)

	//3.方法三
	p3 := Person{
    
    name :"如花",age :20,sex:"女",address:"杭州市"}
	fmt.Println(p3)
	p4 := Person{
    
    
		name:"隔壁老王",
		age : 40,
		sex :"男",
		address:"武汉市",
	}
	fmt.Println(p4)
	
	//4.方法四
    p5 := Person{
    
    "李小花",25,"女","成都"}
    fmt.Println(p5)
}

//1.定义结构体
type Person struct {
    
    
	name string
	age int
	sex string
	address string
}

结构体指针

package main

import "fmt"

func main() {
    
    
	/*
	数据类型:
		值类型:int,float,bool,string,array,struct

		引用类型:slice,map,function,pointer


	通过指针:
		new(),不是nil,空指针
			指向了新分配的类型的内存空间,里面存储的零值。
	 */
	 //1.结构体是值类型
	 p1 := Person{
    
    "王二狗",30,"男","北京市"}
	 fmt.Println(p1)
	 fmt.Printf("%p,%T\n",&p1,p1)//结构体的类型时main包下的结构体

	 p2 := p1
	 fmt.Println(p2)
	 fmt.Printf("%p,%T\n",&p2,p2)

	 p2.name = "李小花"
	 fmt.Println(p2)
	 fmt.Println(p1)

	 //2.定义结构体指针
	 var pp1 *Person
	 pp1 = &p1
	 fmt.Println(pp1)
	 fmt.Printf("%p,%T\n",pp1,pp1)
	 fmt.Println(*pp1)

	//(*pp1).name = "李四"
	pp1.name = "王五"   //结构体也是一种容器,结构体指针变量指向的就是结构体的地址
	fmt.Println(pp1)
	fmt.Println(p1)

	//使用内置函数new(),go语言中专门用于创建某种类型的指针的函数
	pp2 := new(Person)   //先创建个结构体再创建个结构体指针
	fmt.Printf("%T\n",pp2)
	fmt.Println(pp2)
	//(*pp2).name
	pp2.name = "Jerry"
	pp2.age = 20
	pp2.sex = "男"
	pp2.address= "上海市"
	fmt.Println(pp2)

	pp3 := new(int)
	fmt.Println(pp3)
	fmt.Println(*pp3)
}
type Person struct {
    
    
	name string
	age int
	sex string
	address string
}

匿名结构体和结构体的匿名字段

package main

import "fmt"

func main() {
    
    
	/*
	匿名结构体和匿名字段:

	匿名结构体:没有名字的结构体,
		在创建匿名结构体时,同时创建对象
		变量名 := struct{
    
    
			定义字段Field
		}{
    
    
			字段进行赋值
		}

	匿名字段:一个结构体的字段没有字段名


	匿名函数:

	 */
	s1 := Student{
    
    name:"张三",age:18}
	fmt.Println(s1.name,s1.age)

	func (){
    
    
		fmt.Println("hello world...")
	}()

	s2 := struct{
    
    
		name string
		age int
	}{
    
    
		name:"李四",
		age:19,
	}
	fmt.Println(s2.name,s2.age)

	//w1 := Worker{
    
    name:"王二狗",age:30}
	//fmt.Println(w1.name,w1.age)


	w2 := Worker{
    
    "李小花",32}
	fmt.Println(w2)
	fmt.Println(w2.string)
	fmt.Println(w2.int)
}

type Worker struct {
    
    
	//name string
	//age int
	string //匿名字段
	int //匿名字段,默认使用数据类型作为名字,那么匿名字段的类型就不能重复,否则会冲突
	 //string
}

type Student struct {
    
    
	name string
	age int
}

结构体的嵌套

package main

import "fmt"

func main() {
    
    
	/*
	结构体嵌套:一个结构体中的字段,是另一个结构体类型。
		has a
	 */

	 b1 := Book{
    
    }
	 b1.bookName = "西游记"
	 b1.price = 45.8

	 s1 := Student{
    
    }
	 s1.name = "王二狗"
	 s1.age = 18
	 s1.book = b1  //值传递
	 fmt.Println(b1)
	 fmt.Println(s1)
	 fmt.Printf("学生姓名:%s,学生年龄:%d,看的书是:《%s》,书的价格是:%.2f\n",s1.name,s1.age,s1.book.bookName,s1.book.price)


	 s1.book.bookName = "红楼梦"
	 fmt.Println(s1)
	 fmt.Println(b1)


	 b4 := Book{
    
    bookName:"呼啸山庄",price:76.9}
	 s4 := Student2{
    
    name:"Ruby",age:18,book:&b4}
	 fmt.Println(b4)
	 fmt.Println(s4)
	 fmt.Println("\t",s4.book)

	 s4.book.bookName = "雾都孤儿"
	 fmt.Println(b4)
	 fmt.Println(s4)
	 fmt.Println("\t",s4.book)


	 s2 := Student{
    
    name:"李小花",age:19,book:Book{
    
    bookName:"Go语言是怎样炼成的",price:89.7}}
	 fmt.Println(s2.name,s2.age)
	 fmt.Println("\t",s2.book.bookName,s2.book.price)

	 s3 := Student{
    
    
	 	name:"Jerry",
	 	age:17,
	 	book:Book{
    
    
	 		bookName:"十万个为啥",
	 		price:55.9,
		},
	 }
	 fmt.Println(s3.name,s3.age)
	 fmt.Println("\t",s3.book.bookName,s3.book.price)
}

//1.定义一个书的结构体
type Book struct {
    
    
	bookName string
	price float64
}

//2.定义学生的结构体
type Student struct {
    
    
	name string
	age int
	book Book
}

type Student2 struct {
    
    
	name string
	age int
	book *Book // book的地址
}

结构体的OOP

结构体实现面向对象

package main

import "fmt"

func main() {
    
    
	/*
	面向对象:OOP

	Go语言的结构体嵌套:
		1.模拟继承性:is - a
			type A struct{
    
    
				field
			}
			type B struct{
    
    
				A //匿名字段
			}

		2.模拟聚合关系:has - a
			type C struct{
    
    
				field
			}
			type D struct{
    
    
				c C //聚合关系
			}
	 */

	 //1.创建父类的对象
	 p1 := Person{
    
    name:"张三",age:30}
	 fmt.Println(p1)
	 fmt.Println(p1.name,p1.age)

	 //2.创建子类的对象
	s1 := Student{
    
    Person{
    
    "李四",17},"千锋教育"}
	fmt.Println(s1)

	s2 :=Student{
    
    Person:Person{
    
    name:"rose",age:19},school:"北京大学"}
	fmt.Println(s2)

	var s3 Student
	s3.Person.name = "王五"
	s3.Person.age = 19
	s3.school = "清华大学"
	fmt.Println(s3)

	s3.name = "Ruby"   //Person作为Student的匿名字段(结构体作为结构体匿名字段,会有提升字段的效果,即原本访问name得是s3.Person.name,现在直接用s3.name即可
	s3.age = 16
	fmt.Println(s3)

	fmt.Println(s1.name,s1.age,s1.school)
	fmt.Println(s2.name,s2.age,s2.school)
	fmt.Println(s3.name,s3.age,s3.school)
/*
s3.Person.name---->s3.name
Student结构体将Person结构体作为一个匿名字段了
那么Person中的字段,对于Student来讲,就是提升字段
Student对象直接访问Person中的字段
 */


}
//1.定义父类
type Person struct {
    
    
	name string
	age int
}

//2.定义子类
type Student struct {
    
    
	Person //模拟继承结构
	school string //子类的新增属性
}

在这里插入图片描述
面向对象是一种思想,相对于面向过程而言,比如c,面向过程关系程序的每一步
面向过程就是让人假想自己是机器,怎么去实现功能,每一步该怎么做,主要是通过一个一个函数和调用去实现。
面向对象是将生活中的概念搬进程序,不在关系每一步,是看一个一个对象,用类来实例化得到对象,类中主要是属性(字段)和行为(方法),比如一个的个人信息和吃饭等行为能力。

go不是存面向对象的语言,面向对象的特征可以用结构体模仿出来。注意go也不是存面向过程,所以不具备面向过程的一些特性比如完全的自顶向下。

对于面向对象和面向过程,go两者都不倾向,而是让编程人员考虑该怎么去用,特色是灵活,也能够用他来实现面向对象的编程

方法

方法基础

方法与函数很类似
方法是包含接受者的函数
命名类型也就是常见的int之类的,一般用结构体和结构体指针比较多

package main

import "fmt"

func main() {
    
    
	/*
	方法:method
		一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
		所有给定类型的方法属于该类型的方法集


	语法:
		func (接受者) 方法名(参数列表)(返回值列表){
    
    

		}

	总结:method,同函数类似,区别需要有接受者。(也就是调用者)

	对比函数:
		A:意义
			方法:某个类别的行为功能,需要指定的接受者调用
			函数:一段独立功能的代码,可以直接调用

		B:语法
			方法:方法名可以相同,只要接受者不同
			函数:命名不能冲突

	 */
	 w1 := Worker{
    
    name:"王二狗",age:30,sex:"男"}
	 w1.work()

	 w2 := &Worker{
    
    name:"Ruby",age:34,sex:"女"}
	 fmt.Printf("%T\n",w2)
	 w2.work()

	 w2.rest()//不管方法定义时定义的接受者是结构体还是结构体指针,这里w1这个结构体变量和w2这个结构体指针代表的接受着一样
	 w1.rest()

	 w2.printInfo()
	 c1 := Cat{
    
    color:"白色的",age:1}
	 c1.printInfo()

}

//1.定义一个工人结构体
type Worker struct {
    
    
	//字段
	name string
	age int
	sex string
}

type Cat struct {
    
    
	color string
	age int
}

//2.定义行为方法

func (w Worker) work(){
    
     //w = w1,值传递
	fmt.Println(w.name,"在工作。。。")
}

func (p *Worker)rest(){
    
     //p = w2 ,p = w1的地址
	fmt.Println(p.name,"在休息。。")
}

//同样名称的方法不同接受者调用效果不同,可以在调用处Ctrl+B跳转到实际使用的方法的调用处
func (p *Worker)printInfo(){
    
    
	fmt.Printf("工人姓名:%s,工人年龄:%d,工人性别:%s\n",p.name,p.age,p.sex)
}

func (p *Cat)printInfo(){
    
    
	fmt.Printf("猫咪的颜色:%s,年龄:%d\n",p.color,p.age)
}

方法继承

package main

import "fmt"

func main() {
    
    
	/*
	OOP中的继承性:
		如果两个类(class)存在继承关系,其中一个是子类,另一个作为父类,那么:

		1.子类可以直接访问父类的属性和方法
		2.子类可以新增自己的属性和方法
		3.子类可以重写父类的方法(orverride,就是将父类已有的方法,重新实现)


	Go语言的结构体嵌套:
		1.模拟继承性:is - a
			type A struct{
    
    
				field
			}
			type B struct{
    
    
				A //匿名字段
			}

		2.模拟聚合关系:has - a
			type C struct{
    
    
				field
			}
			type D struct{
    
    
				c C //聚合关系
			}

	 */
	 //1.创建Person类型
	 p1 := Person{
    
    name:"王二狗",age:30}
	 fmt.Println(p1.name,p1.age) //父类对象,访问父类的字段属性
	 p1.eat() //父类对象,访问父类的方法

	 //2.创建Student类型
	 s1 := Student{
    
    Person{
    
    "Ruby",18},"千锋教育"}
	 fmt.Println(s1.name) //s1.Person.name
	 fmt.Println(s1.age) //子类对象,可以直接访问父类的字段,(其实就是提升字段)
	 fmt.Println(s1.school) //子类对象,访问自己新增的字段属性

	 s1.eat() //子类对象,访问父类的方法
	 s1.study() //子类对象,访问自己新增的方法
	 s1.eat() //如果存在方法的重写,子类对象访问重写的方法
}

//1.定义一个"父类"
type Person struct {
    
    
	name string
	age int
}

//2.定义一个"子类"
type Student struct {
    
    
	Person //结构体嵌套,模拟继承性
	school string
}

//3.方法
func (p Person) eat(){
    
    
	fmt.Println("父类的方法,吃窝窝头。。")
}

func (s Student) study(){
    
    
	fmt.Println("子类新增的方法,学生学习啦。。。")
}

func (s Student) eat(){
    
    
	fmt.Println("子类重写的方法:吃炸鸡喝啤酒。。")
}

模拟出面向对象的特性

接口

接口介绍

接口是一组方法签名
当类型为接口的所有方法提供定义是,称为实现接口

也就是说接口定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象实现接口

package main

import "fmt"

func main() {
    
    
	/*
	接口:interface
		在Go中,接口是一组方法签名。

		当某个类型为这个接口中的所有方法提供了方法的实现,它被称为实现接口。

		Go语言中,接口和类型的实现关系,是非侵入式

		//其他语言中,要显示的定义
		class Mouse implements USB{
    
    }

	1.当需要接口类型的对象时,可以使用任意实现类对象代替
	2.接口对象不能访问实现类中的属性


	多态:一个事物的多种形态
		go语言通过接口模拟多态

		就一个接口的实现
			1.看成实现本身的类型,能够访问实现类中的属性和方法
			2.看成是对应的接口类型,那就只能够访问接口中的方法

	接口的用法:
		1.一个函数如果接受接口类型作为参数,那么实际上可以传入该接口的任意实现类型对象作为参数。
		2.定义一个类型为接口类型,实际上可以赋值为任意实现类的对象


	鸭子类型:

	 */
	 //1.创建Mouse类型
	 m1 := Mouse{
    
    "罗技小红"}
	 fmt.Println(m1.name)
	 //2.创建FlashDisk
	 f1 := FlashDisk{
    
    "闪迪64G"}
	 fmt.Println(f1.name)

	 testInterface(m1)
	 testInterface(f1)

	 var usb USB
	 usb= f1
	 usb.start()
	 usb.end()
	 //fmt.Println(usb.name)

	 f1.deleteData()
	 //usb.de

	 var arr [3]USB
	 arr[0] = m1
	 arr[1] = f1
	 fmt.Println(arr)
}

//1.定义接口
type USB interface {
    
    
	start() //USB设备开始工作,可以有参数和返回值,没有可以不写
	end() //USB设备结束工作
}

//2.实现类
type Mouse struct {
    
    
	name string
}

type FlashDisk struct {
    
    
	name string
}

func (m Mouse)start(){
    
    
	fmt.Println(m.name,"鼠标,准备就绪,可以开始工作了,点点点。。")
}
func (m Mouse) end(){
    
    
	fmt.Println(m.name,"结束工作,可以安全退出。。")
}

func (f FlashDisk)start(){
    
    
	fmt.Println(f.name,"准备开始工作,可以进行数据的存储。。")
}
func (f FlashDisk)end(){
    
    
	fmt.Println(f.name,"可以弹出。。")
}

//3.测试方法
func testInterface(usb USB){
    
     //usb = m1  usb = f1
	usb.start()
	usb.end()
}

func (f FlashDisk) deleteData(){
    
    
	fmt.Println(f.name,"U盘删除数据。。")
}

空接口

package main

import "fmt"

func main() {
    
    
	/*
	空接口(interface{
    
    })
		不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。

	fmt包下的Print系列函数:
		func Print(a ...interface{
    
    }) (n int, err error)
		func Printf(format string, a ...interface{
    
    }) (n int, err error)
		func Println(a ...interface{
    
    }) (n int, err error)
	 */
	var a1 A = Cat{
    
    "花猫"}
	var a2 A = Person{
    
    "王二狗",30}
	var a3 A = "haha"
	var a4 A = 100
	fmt.Println(a1)
	fmt.Println(a2)
	fmt.Println(a3)
	fmt.Println(a4)
	test1(a1)
	test1(a2)
	test1(3.14)
	test1("Ruby")

	test2(a3)
	test2(1000)

	//map,key字符串,value任意类型
	map1 := make(map[string]interface{
    
    })
	map1["name"] = "李小花"
	map1["age"] = 30
	map1["friend"] = Person{
    
    "Jerry",18}
	fmt.Println(map1)

	//切片,存储任意类型的数据
	slice1 := make([]interface{
    
    },0,10)
	slice1 = append(slice1,a1,a2,a3,a4,100,"abc")
	fmt.Println(slice1)

	test3(slice1)

}

func test3(slice2 []interface{
    
    }){
    
    
	for i:=0;i<len(slice2);i++{
    
    
		fmt.Printf("第%d个数据:%v\n",i+1,slice2[i])
	}
}

//接口A是空接口,理解为代表了任意类型
func test1(a A){
    
    
	fmt.Println(a)
}

func test2(a interface{
    
    }){
    
    
	fmt.Println("--->",a)
}

//空接口
type A interface {
    
    

}
type Cat struct {
    
    
	color string
}
type Person struct {
    
    
	name string
	age int
}

接口嵌套

可以理解成接口的继承

package main

import "fmt"

func main() {
    
    
	/*
	接口的嵌套:
	 */
	var cat Cat = Cat{
    
    }
	cat.test1()
	cat.test2()
	cat.test3()

	fmt.Println("--------------")
	var a1 A = cat
	a1.test1()

	fmt.Println("--------------")
	var b1 B = cat
	b1.test2()
	fmt.Println("--------------")
	var c1 C = cat
	c1.test1()
	c1.test2()
	c1.test3()

	fmt.Println("----------")
	//var c2 C = a1
	var a2 A = c1
	a2.test1()
}

type A interface {
    
    
	test1()
}

type B interface {
    
    
	test2()
}

type C interface {
    
    
	A
	B
	test3()
}

type Cat struct {
    
    
	//如果想实现接口C,那不止要实现接口C的方法,还要实现接口A,B中的方法
}

func (c Cat) test1() {
    
    
	fmt.Println("test1()....")
}

func (c Cat) test2() {
    
    
	fmt.Println("test2()....")
}

func (c Cat) test3() {
    
    
	fmt.Println("test3()....")
}

接口断言

空接口interface{}没有定义任何的方法,go的所有类型都实现了空接口,当一个函数用空接口作为形参时,要对形参用断言才能获取到它的真实类型

package main

import (
	"math"
	"fmt"
)

func main() {
    
    
	/*
	接口断言:
		方式一:
			1.instance := 接口对象.(实际类型) //不安全,会panic()
			2.instance, ok := 接口对象.(实际类型) //安全

		方式二:switch
			switch instance := 接口对象.(type){
    
    
			case 实际类型1:
					....
			case 实际类型2:
					....
			....
			}
	 */
	 var t1 Triangle = Triangle{
    
    3,4,5}
	 fmt.Println(t1.peri())
	 fmt.Println(t1.area())
	 fmt.Println(t1.a, t1.b,t1.c)

	 var c1 Circle = Circle{
    
    4}
	 fmt.Println(c1.peri())
	 fmt.Println(c1.area())
	 fmt.Println(c1.radius)

	 var s1 Shape
	 s1 = t1
	 fmt.Println(s1.peri())
	 fmt.Println(s1.area())

	 var s2 Shape
	 s2 = c1
	 fmt.Println(s2.peri())
	 fmt.Println(s2.area())

	 testShape(t1)
	 testShape(c1)
	 testShape(s1)

	 getType(t1)
	 getType(c1)
	 getType(s1)
	 //getType(100)

	 var t2 *Triangle = &Triangle{
    
    3,4,2}
	 fmt.Printf("t2:%T,%p,%p\n",t2,&t2,t2)
	 getType(t2)
	 getType2(t2)
	 getType2(t1)

}

func getType2 (s Shape){
    
    
	switch ins := s.(type) {
    
    
	case Triangle:
		fmt.Println("三角形。。",ins.a,ins.b,ins.c)
	case Circle:
		fmt.Println("圆形。。",ins.radius)
	case *Triangle:
		fmt.Println("三角形结构体指针:",ins.a,ins.b,ins.c)
	}
}
func getType(s Shape){
    
    
	//断言
	if ins, ok := s.(Triangle) ; ok{
    
    
		fmt.Println("是三角形,三边是:",ins.a,ins.b,ins.c)
	}else if ins, ok := s.(Circle); ok{
    
    
		fmt.Println("是圆形,半径是:",ins.radius)
	}else if ins, ok := s.(*Triangle) ;ok{
    
    
		fmt.Printf("ins:%T,%p,%p\n",ins,&ins,ins)
		fmt.Printf("s:%T,%p,%p\n",s,&s,s)
	}else {
    
    
		fmt.Println("我也不知道了。。。")

	}
}

func testShape(s Shape){
    
    
	fmt.Printf("周长:%.2f,面积:%.2f\n",s.peri(),s.area())
}
//1.定义一个接口
type Shape interface {
    
    
	peri() float64 //形状的周长
	area() float64 //形状的面积
}

//2.定义实现类:三角形
type Triangle struct {
    
    
	//a float64
	//b float64
	//c float64
	a, b, c float64
}

func (t Triangle) peri() float64  {
    
    
	return t.a + t.b + t.c
}

func (t Triangle) area() float64  {
    
    
	p := t.peri() / 2
	s := math.Sqrt(p * (p-t.a)*(p-t.b)*(p-t.c))
	return s
}

type Circle struct {
    
    
	radius float64
}

func (c Circle) peri()float64  {
    
    
	return c.radius * 2 * math.Pi
}
func (c Circle) area () float64{
    
    
	return math.Pow(c.radius,2) * math.Pi
}

type关键字

用来定义数据类型,一般是用来定义接口和结构体,也可以用来定义别名

package main

import (
	"fmt"
	"strconv"
)

func main() {
    
    
	/*
	type:用于类型定义和类型别名

		1.类型定义:type 类型名 Type
		2.类型别名:type  类型名 = Type

	 */
	var i1 myint
	var i2 = 100 //int
	i1 = 200
	fmt.Println(i1,i2)

	var name mystr
	name = "王二狗"
	var s1 string
	s1 = "李小花"
	fmt.Println(name,s1)

	//i1 = i2 //cannot use i2 (type int) as type myint in assignment

	//name = s1 //cannot use s1 (type string) as type mystr in assignment

	fmt.Printf("%T,%T,%T,%T\n",i1,i2,name,s1) //main.myint,int,main.mystr,string

	fmt.Println("----------------------------------")
	res1 := fun1()
	fmt.Println(res1(10,20))

	fmt.Println("----------------------------------")
	var i3 myint2
	i3 = 1000
	fmt.Println(i3)
	i3 = i2
	fmt.Println(i3)
	fmt.Printf("%T,%T,%T\n",i1,i2,i3) //main.myint,int,int

}
//1.定义一个新的类型
type myint int
type mystr string


//2.定义函数类型
type myfun func(int,int)(string)

func fun1() myfun{
    
    //fun1()函数的返回值是myfun类型
	fun := func(a, b int)string {
    
    
		s := strconv.Itoa(a) + strconv.Itoa(b)
		return s
	}
	return fun
}

//3.类型别名
type myint2 = int //不是重新定义新的数据类型,只是给int起别名,和int可以通用

错误与异常

错误基础与错误类型展示

错误是意料之中,就是在可能出现问题的地方出现的问题,比如打开一个文件时打开文件失败,就是一种错误

异常是意料之外,在不应该出现问题的地方出现了问题,比如引用了空指针

错误是业务流程的一部分,而异常不是,错误是一种类型,用内置error类型表示,错误值可以存储在变量中,从函数中返回等等,就和其他的类型一样
os包这些的函数一般都会有返回个错误服务

package main

import (
	"os"
	"fmt"
)

//如果出现错误会报错同时中断代码的运行
func main() {
    
    
	f,err := os.Open("test.txt")   //想看这个包的这个函数,Ctrl键点击Open
	if err != nil{
    
    
		//log.Fatal(err)
		fmt.Println(err) //open test.txt: no such file or directory,这只是简单打印了错误的描述
		if ins ,ok := err.(*os.PathError);ok{
    
    
			fmt.Println("1.Op:",ins.Op)
			fmt.Println("2.Path:",ins.Path)
			fmt.Println("3.Err:",ins.Err)
		}
		return
	}
	fmt.Println(f.Name(),"打开文件成功。。")

}

如果一个函数或者方法返回一个错误,按照惯例,它必须是函数返回的最后一个值
处理错误的管用方法是将返回的错误和nil进行比较,nil表示没有错误,非nil表示出现错误

package main

import (
	"errors"
	"fmt"
)

func main() {
    
    
	/*
	error:内置的数据类型,内置的接口
		定义方法:Error() string

	使用go语言提供好的包:
		errors包下的函数:New(),创建一个error对象
		fmt包下的Errorf()函数:
			func Errorf(format string, a ...interface{
    
    }) error
	 */
	//1.创建一个error数据
	err1 := errors.New("自己创建玩的。。")
	fmt.Println(err1)
	fmt.Printf("%T\n",err1) //*errors.errorString

	//2.另一个创建error的方法
	err2 := fmt.Errorf("错误的信息码:%d",100)
	fmt.Println(err2)
	fmt.Printf("%T\n",err2)

	fmt.Println("-----------------")
	err3 := checkAge(-30)
	if err3 != nil{
    
    
		fmt.Println(err3)
		return
	}
	fmt.Println("程序。。。go on。。。")
}
//设计一个函数:验证年龄是否合法,如果为负数,就返回一个error
func checkAge(age int) error{
    
    
	if age < 0{
    
    
		//返回error对象
		//return errors.New("年龄不合法")
		err := fmt.Errorf("您给定的年龄是:%d,不合法",age)
		return err
	}
	fmt.Println("年龄是:",age)
	return nil
}
package main

import (
	"net"
	"fmt"
)

func main() {
    
    
	addr,err := net.LookupHost("www.baidu.com")
	fmt.Println(err)
	if ins, ok := err.(*net.DNSError);ok{
    
    
		if ins.Timeout(){
    
    
			fmt.Println("操作超时。。")
		}else if ins.Temporary(){
    
    
			fmt.Println("临时性错误。。")
		}else{
    
    
			fmt.Println("通常错误。。")
		}
	}
	fmt.Println(addr)
}
package main

import (
	"path/filepath"
	"fmt"
)

func main() {
    
    
	files,_:=filepath.Glob("[")
	//if err != nil && err == filepath.ErrBadPattern{
    
    
	//	fmt.Println(err) //syntax error in pattern
	//	return
	//}
	fmt.Println("files:",files)
}

自定义错误

package main

import (
	"fmt"
	"math"
)

func main() {
    
    
	/*
	自定义错误:
	 */
	 radius := -3.0
	 area,err := circleArea(radius)
	 if err != nil{
    
    
	 	fmt.Println(err)
	 	if err ,ok := err.(*areaError);ok{
    
    
	 		fmt.Printf("半径是:%.2f\n",err.radius)
		}
	 	return
	 }
	 fmt.Println("圆形的面积是:",area)

}

//1.定义一个结构体,表示错误的类型
type areaError struct {
    
    
	msg string
	radius float64
}

//2.实现error接口,就是实现Error()方法
func (e *areaError) Error() string{
    
    
	return fmt.Sprintf("error:半径,%.2f,%s",e.radius,e.msg)
}

func circleArea(radius float64)(float64,error){
    
    
	if radius <0 {
    
    
		return 0,&areaError{
    
    "半径是非法的",radius}
	}
	return math.Pi * radius * radius,nil
}

panic和recover

package main

import "fmt"

func main() {
    
    
	/*
	panic:词义"恐慌",
	recover:"恢复"
	go语言利用panic(),recover(),实现程序中的极特殊的异常的处理
		panic(),让当前的程序进入恐慌,中断程序的执行
		recover(),让程序恢复,必须在defer函数中执行
	 */
	defer func(){
    
    
		if msg := recover();msg != nil{
    
    
			fmt.Println(msg,"程序回复啦。。。")
		}
	}()
	funA()
	defer myprint("defer main:3.....")
	funB()
	defer myprint("defer main:4.....")

	fmt.Println("main..over。。。。")

}
func myprint(s string){
    
    
	fmt.Println(s)
}

func funA(){
    
    
	fmt.Println("我是一个函数funA()....")
}

func funB(){
    
    //外围函数

	fmt.Println("我是函数funB()...")
	defer myprint("defer funB():1.....")

	for i:= 1;i<=10;i++{
    
    
		fmt.Println("i:",i)
		if i == 5{
    
    
			//让程序中断
			panic("funB函数,恐慌了")
		}
	}//当外围函数的代码中发生了运行恐慌,只有其中所有的已经defer的函数全部都执行完毕后,该运行恐慌才会真正被扩展至调用处。
	defer myprint("defer funB():2.....")
}

包的管理

main包:go语言的入口函数所在的包叫main包,main包想要引用别的代码,需要import
package:src目录一代码包的形式组织并保存go源码文件
报名不一定要和目录名相同,但一般建议些相同
同一个目录下的所有源码文件的第一行声明的包名必须一致(同一个目录下不能声明多个包)

源码文件中的函数名变量名等等首字母大写的话表示可以跨包使用,也就是别的源码文件导入包之后可以使用这些变量函数等,注意结构体中的,如果结构体可以跨包,它的成员也要满足大写才能被调用

goland可以自动导包

import导包是,会从GOROOT和GOPATH环境变量定义的目录中,检索src下的包(GOROOT也有src),当然可以一层一层,比如src下的目录下还有目录等等,就是目录的嵌套

导包可以是绝对路也可以是相对路径,相对路径就是./开头,以当前所处路径为基准

绝对路径其实也是相对于src目录开始的
在这里插入图片描述
init函数一般是用来做初始化的,和main()函数一样不能有参数也不能有返回值

main.go

package main

import (
	_ "l_package/pk1"

	//"database/sql"
	//_ "github.com/go-sql-driver/mysql"
	//"fmt"
	"l_package/pk2"
)

//import "l_package/utils" //绝对路径

//import "./utils" //相对路径
//
//import (
//	"l_package/utils/timeutils"
//	"l_package/pk1"
//	"fmt"
//	p "l_package/person"
//)
func main() {
    
    
	/*
		关于包的使用:
		1.一个目录下的统计文件归属一个包。package的声明要一致
		2.package声明的包和对应的目录名可以不一致。但习惯上还是写成一致的
		3.包可以嵌套
		4.同包下的函数不需要导入包,可以直接使用
		5.main包,main()函数所在的包,其他的包不能使用
		6.导入包的时候,路径要从src下开始写
	*/
	//utils.Count()
	//timeutils.PrintTime()
	//
	//pk1.MyTest1()
	//
	//utils.MyTest2()
	//
	//fmt.Println("---------------------")
	//p1 :=p.Person{
    
    Name:"王二狗",Sex:"男"}
	//fmt.Println(p1.Name,p1.Sex)

	/*
		init()函数和main()函数
		1.这两个函数都是go语言中的保留函数。init()用于初始化信息,main()用于作为程序的入口
		2.这两个函数定义的时候:不能有参数,返回值。只能由go程序自动调用,不能被引用。
		3.init()函数可以定义在任意的包中,可以有多个。main()函数只能在main包下,并且只能有一个。
		4.执行顺序
			A:先执行init()函数,后执行main()函数
			B:对于同一个go文件中,调用顺序是从上到下的,也就是说,先写的先被执行,后写的后被执行
			C:对于同一个包下,将文件名按照字符串进行排序,之后顺序调用各个文件中init()函数
			D:对于不同包下,
				如果不存在依赖,按照main包中import的顺序来调用对应包中init()函数
				如果存在依赖,最后被依赖的最先被初始化
					导入顺序:main-->A-->B-->C
					执行顺序:C-->B-->A-->main

		5.存在依赖的包之间,不能循环导入
		6.一个包可以被其他多个包import,但是只能被初始化一次。
		7._操作,其实是引入该包,而不直接使用包里面的函数,仅仅是调用了该包里的init()
	*/
	//utils.Count()

	//pk1.MyTest1()

	//database/sql

	//db,err :=sql.Open("mysql","root:hanru1314@tcp(127.0.0.1:3306)/my1802?charset=utf8")
	//if err != nil{
    
    
	//	fmt.Println("错误信息:",err)
	//	return
	//}
	//fmt.Println("链接成功:",db)

	pk2.MyTest3()
}


timedemo.go
package main

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

func main() {
    
    
	/*
	time包:
		1=365天,day
		1=24小时,hour
		1小时=60分钟,minute
		1分钟=60秒,second
		1秒钟=1000毫秒,millisecond
		1毫秒=1000微秒,microsecond-->μs
		1微秒=1000纳秒,nanosecond-->ns
		1纳秒=1000皮秒,picosecond-->ps
	 */

	 //1.获取当前的时间
	 t1 := time.Now()
	 fmt.Printf("%T\n",t1)
	 fmt.Println(t1) //2019-06-26 10:46:40.349196 +0800 CST m=+0.000495846

	 //2.获取指定的时间
	 t2 := time.Date(2008,7,15,16,30,28,0,time.Local)
	 fmt.Println(t2) //2008-07-15 16:30:28 +0800 CST

	 //3.time-->string之间的转换
	 /*
	 t1.Format("格式模板")-->string
	 	模板的日期必须是固定:06-1-2-3-4-5
	  */
	 s1 := t1.Format("2006-1-2 15:04:05")
	 fmt.Println(s1)

	 s2 := t1.Format("2006/01/02")
	 fmt.Println(s2)

	 //string-->time
	 /*
	 time.Parse("模板",str)-->time,err
	  */
	 s3 := "1999年10月10日"//string
	 t3 ,err := time.Parse("2006年01月02日",s3)
	 if err != nil{
    
    
	 	fmt.Println("err:",err)
	 }
	 fmt.Println(t3)
	 fmt.Printf("%T\n",t3)

	 fmt.Println(t1.String())

	 //4.根据当前时间,获取指定的内容
	year,month,day :=  t1.Date() //年,月,日
	fmt.Println(year,month,day)

	hour,min,sec := t1.Clock()
	fmt.Println(hour,min,sec) //时,分,秒

	year2 := t1.Year()
	fmt.Println("年:",year2)
	fmt.Println(t1.YearDay())

	month2 := t1.Month()
	fmt.Println("月:",month2)
	fmt.Println("日:",t1.Day())
	fmt.Println("时:",t1.Hour())
	fmt.Println("分钟:",t1.Minute())
	fmt.Println("秒:",t1.Second())
	fmt.Println("纳秒:",t1.Nanosecond())

	fmt.Println(t1.Weekday()) //Wednesday

	//5.时间戳:指定的日期,距离1970年1月1日0点0时0分0秒的时间差值:秒,纳秒

	t4 := time.Date(1970,1,1,1,0,0,0,time.UTC)
	timeStamp1:=t4.Unix() //秒的差值
	fmt.Println(timeStamp1)
	timeStamp2 := t1.Unix()
	fmt.Println(timeStamp2)

	timeStamp3 := t4.UnixNano()
	fmt.Println(timeStamp3) //3600 000 000 000
	timeStamp4 := t1.UnixNano()
	fmt.Println(timeStamp4)

	//6.时间间隔
	t5 := t1.Add(time.Minute)
	fmt.Println(t1)
	fmt.Println(t5)
	fmt.Println(t1.Add(24 * time.Hour))

	t6 := t1.AddDate(1,0,0)
	fmt.Println(t6)

	d1 := t5.Sub(t1)
	fmt.Println(d1)

	//7.睡眠
	time.Sleep(3 *time.Second) //让当前的程序进入睡眠状态
	fmt.Println("main。。。over。。。。。")

	//睡眠[1-10]的随机秒数
	rand.Seed(time.Now().UnixNano())
	randNum := rand.Intn(10) + 1 //int
	fmt.Println(randNum)
	time.Sleep(time.Duration(randNum)*time.Second)
	fmt.Println("睡醒了。。")
}
person.go

package person

type Person struct {
    
    
	Name string
	age int
	Sex string
}
test2.go

package pk1

import (

	"fmt"

)

func MyTest1(){
    
    
	//utils.Count()
	//timeutils.PrintTime()
	fmt.Println("pk1包下的MyTest1()函数。。。。")
}

func init(){
    
    
	fmt.Println("pk1包下的init()函数。。。")
}
test3.go

package pk2

import "fmt"

func MyTest3(){
    
    
	fmt.Println("pk2包下的MyTest3()函数。。。")
}
timeutil.go

package timeutils

import (
	"fmt"
	"time"
)

func PrintTime(){
    
    
	fmt.Println(time.Now())

util.go

package utils

import "fmt"

func Count(){
    
    
	fmt.Println("utils包下的Count()函数。。。")
}

func init(){
    
    
	fmt.Println("utils包的另一个init()函数。。")
}


func init(){
    
    
	fmt.Println("utils包的init()函数,用于初始化一些信息。。。")
}
ytest1.go

package utils

import "fmt"

func MyTest2(){
    
    
	Count()
}

func init(){
    
    
	fmt.Println("utils包下的test1.go文件中的init()函数。。。")
}

init()和main()都是有go自动调用,不能手动引用
init()可以用于多个包,且可以定义多个
main()只能用于main包

同一个go文件下多个init(),调用的顺序自上而下
同一个包下有多个init(),会将文件名按字符串从小到大排序在调用
不同的包下的init(),这些包之前不存在依赖,先被main包import的包先使用它的init(),如果这些包之前存在依赖那么最后被依赖的包(A->B->C的init先调用

以上操作的包都是自己在项目目录或src目录下的包,约等于与自己写的包,接下来是操作外部的包

这里像操作数据库mysql,就需要下对应的包
方法一:
在这里插入图片描述
这里面是go操作数据库的各种代码包
将源码下载下来,解压缩,然后拷贝目录到GOPATH的src下,为了路径清晰,你可以先在src目录下创建个github.com,在github.com下面创建个go-sql-driver,将解压的目录拷贝到这里,改下名字为mysql,mysql文件夹下就有诸多的go文件
(这就是最清晰的导入外部包的方法)

方法二:

go get github.com/go-sql-driver/mysql

自动存放到GOPATH的src下,如果定义了多个GOPATH,则默认放到第一个GOPATH的src下

go自身也有操作数据的包database/sql,只是功能不丰富

goland的邮件RUN其实包含了go install,所以会生成相应的.a文件,在pkg下

go的IO

File文件操作

首先,file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现。

一、FileInfo接口
FileInfo接口中定义了File信息相关的方法。

type FileInfo interface {
    
    
    Name() string       // base name of the file 文件名.扩展名 aa.txt
    Size() int64        // 文件大小,字节数 12540
    Mode() FileMode     // 文件权限 -rw-rw-rw-
    ModTime() time.Time // 修改时间 2018-04-13 16:30:53 +0800 CST
    IsDir() bool        // 是否文件夹
    Sys() interface{
    
    }   // 基础数据源接口(can return nil)
}

二、权限
至于操作权限perm,除非创建文件时才需要指定,不需要创建新文件时可以将其设定为0。虽然go语言给perm权限设定了很多的常量,但是习惯上也可以直接使用数字,如0666(具体含义和Unix系统的一致)。

权限控制:

linux 下有2种文件权限表示方式,即“符号表示”和“八进制表示”。

(1)符号表示方式:
-      ---         ---        ---
type   owner       group      others
文件的权限是这样子分配的 读 写 可执行 分别对应的是 r w x 如果没有那一个权限,用 - 代替
(-文件 d目录 |连接符号)
例如:-rwxr-xr-x

(2)八进制表示方式: 
r ——> 004
w ——> 002
x ——> 001
- ——> 000

0755
0777
0555
0444
0666

三、打开模式
文件打开模式:

const (
    O_RDONLY int = syscall.O_RDONLY // 只读模式打开文件
    O_WRONLY int = syscall.O_WRONLY // 只写模式打开文件
    O_RDWR   int = syscall.O_RDWR   // 读写模式打开文件
    O_APPEND int = syscall.O_APPEND // 写操作时将数据附加到文件尾部
    O_CREATE int = syscall.O_CREAT  // 如果不存在将创建一个新文件
    O_EXCL   int = syscall.O_EXCL   // 和O_CREATE配合使用,文件必须不存在
    O_SYNC   int = syscall.O_SYNC   // 打开文件用于同步I/O
    O_TRUNC  int = syscall.O_TRUNC  // 如果可能,打开时清空文件
)

四、File操作

type File
//File代表一个打开的文件对象。

func Create(name string) (file *File, err error)
//Create采用模式0666(任何人都可读写,不可执行)创建一个名为name的文件,如果文件已存在会截断它(为空文件)。如果成功,返回的文件对象可用于I/O;对应的文件描述符具有O_RDWR模式。如果出错,错误底层类型是*PathError。

func Open(name string) (file *File, err error)
//Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。

func OpenFile(name string, flag int, perm FileMode) (file *File, err error)
//OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。

func NewFile(fd uintptr, name string) *File
//NewFile使用给出的Unix文件描述符和名称创建一个文件。

func Pipe() (r *File, w *File, err error)
//Pipe返回一对关联的文件对象。从r的读取将返回写入w的数据。本函数会返回两个文件对象和可能的错误。

func (f *File) Name() string
//Name方法返回(提供给Open/Create等方法的)文件名称。

func (f *File) Stat() (fi FileInfo, err error)
//Stat返回描述文件f的FileInfo类型值。如果出错,错误底层类型是*PathError。

func (f *File) Fd() uintptr
//Fd返回与文件f对应的整数类型的Unix文件描述符。

func (f *File) Chdir() error
//Chdir将当前工作目录修改为f,f必须是一个目录。如果出错,错误底层类型是*PathError。

func (f *File) Chmod(mode FileMode) error
//Chmod修改文件的模式。如果出错,错误底层类型是*PathError。

func (f *File) Chown(uid, gid int) error
//Chown修改文件的用户ID和组ID。如果出错,错误底层类型是*PathError。

func (f *File) Close() error
//Close关闭文件f,使文件不能用于读写。它返回可能出现的错误。

func (f *File) Readdir(n int) (fi []FileInfo, err error)
//Readdir读取目录f的内容,返回一个有n个成员的[]FileInfo,这些FileInfo是被Lstat返回的,采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0,Readdir函数会返回一个最多n个成员的切片。这时,如果Readdir返回一个空切片,它会返回一个非nil的错误说明原因。如果到达了目录f的结尾,返回值err会是io.EOF。如果n<=0,Readdir函数返回目录中剩余所有文件对象的FileInfo构成的切片。此时,如果Readdir调用成功(读取所有内容直到结尾),它会返回该切片和nil的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的FileInfo构成的切片和该错误。

func (f *File) Readdirnames(n int) (names []string, err error)
//Readdir读取目录f的内容,返回一个有n个成员的[]string,切片成员为目录中文件对象的名字,采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0,Readdir函数会返回一个最多n个成员的切片。这时,如果Readdir返回一个空切片,它会返回一个非nil的错误说明原因。如果到达了目录f的结尾,返回值err会是io.EOF。如果n<=0,Readdir函数返回目录中剩余所有文件对象的名字构成的切片。此时,如果Readdir调用成功(读取所有内容直到结尾),它会返回该切片和nil的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。

func (f *File) Truncate(size int64) error
//Truncate改变文件的大小,它不会改变I/O的当前位置。 如果截断文件,多出的部分就会被丢弃。如果出错,错误底层类型是*PathError。

五、示例代码
文件信息:FileInfo

package main

import (
    "os"
    "fmt"
)

func main() {
    
    
    /*
    FileInfo:文件信息
        interface
            Name(),文件名
            Size(),文件大小,字节为单位
            IsDir(),是否是目录
            ModTime(),修改时间
            Mode(),权限

     */
    fileInfo,err :=  os.Stat("/Users/ruby/Documents/pro/a/aa.txt")
    if err != nil{
    
    
        fmt.Println("err :",err)
        return
    }
    fmt.Printf("%T\n",fileInfo)
    //文件名
    fmt.Println(fileInfo.Name())
    //文件大小
    fmt.Println(fileInfo.Size())
    //是否是目录
    fmt.Println(fileInfo.IsDir()) //IsDirectory
    //修改时间
    fmt.Println(fileInfo.ModTime())
    //权限
    fmt.Println(fileInfo.Mode()) //-rw-r--r--
}

文件操作实例:

package main

import (
    "fmt"
    "path/filepath"
    "path"
    "os"
)

func main() {
    
    
    /*
    文件操作:
    1.路径:
        相对路径:relative
            ab.txt
            相对于当前工程
        绝对路径:absolute
            /Users/ruby/Documents/pro/a/aa.txt

        .当前目录
        ..上一层
    2.创建文件夹,如果文件夹存在,创建失败
        os.MkDir(),创建一层
        os.MkDirAll(),可以创建多层

    3.创建文件,Create采用模式0666(任何人都可读写,不可执行)创建一个名为name的文件,如果文件已存在会截断它(为空文件)
        os.Create(),创建文件

    4.打开文件:让当前的程序,和指定的文件之间建立一个连接
        os.Open(filename)
        os.OpenFile(filename,mode,perm)

    5.关闭文件:程序和文件之间的链接断开。
        file.Close()

    5.删除文件或目录:慎用,慎用,再慎用
        os.Remove(),删除文件和空目录
        os.RemoveAll(),删除所有
     */
     //1.路径
     fileName1:="/Users/ruby/Documents/pro/a/aa.txt"
     fileName2:="bb.txt"
     fmt.Println(filepath.IsAbs(fileName1)) //true
     fmt.Println(filepath.IsAbs(fileName2)) //false
     fmt.Println(filepath.Abs(fileName1))
     fmt.Println(filepath.Abs(fileName2)) // /Users/ruby/go/src/l_file/bb.txt

     fmt.Println("获取父目录:",path.Join(fileName1,".."))

     //2.创建目录
     //err := os.Mkdir("/Users/ruby/Documents/pro/a/bb",os.ModePerm)
     //if err != nil{
    
    
     // fmt.Println("err:",err)
     // return
     //}
     //fmt.Println("文件夹创建成功。。")
     //err :=os.MkdirAll("/Users/ruby/Documents/pro/a/cc/dd/ee",os.ModePerm)
     //if err != nil{
    
    
     // fmt.Println("err:",err)
     // return
     //}
     //fmt.Println("多层文件夹创建成功")

     //3.创建文件:Create采用模式0666(任何人都可读写,不可执行)创建一个名为name的文件,如果文件已存在会截断它(为空文件)
     //file1,err :=os.Create("/Users/ruby/Documents/pro/a/ab.txt")
     //if err != nil{
    
    
     // fmt.Println("err:",err)
     // return
     //}
     //fmt.Println(file1)

     //file2,err := os.Create(fileName2)//创建相对路径的文件,是以当前工程为参照的
     //if err != nil{
    
    
     // fmt.Println("err :",err)
     // return
     //}
     //fmt.Println(file2)

     //4.打开文件:
     //file3 ,err := os.Open(fileName1) //只读的
     //if err != nil{
    
    
     // fmt.Println("err:",err)
     // return
     //}
     //fmt.Println(file3)
    /*
    第一个参数:文件名称
    第二个参数:文件的打开方式
        const (
    // Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
        O_RDONLY int = syscall.O_RDONLY // open the file read-only.
        O_WRONLY int = syscall.O_WRONLY // open the file write-only.
        O_RDWR   int = syscall.O_RDWR   // open the file read-write.
        // The remaining values may be or'ed in to control behavior.
        O_APPEND int = syscall.O_APPEND // append data to the file when writing.
        O_CREATE int = syscall.O_CREAT  // create a new file if none exists.
        O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist.
        O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.
        O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened.
    )
    第三个参数:文件的权限:文件不存在创建文件,需要指定权限
     */
     //file4,err := os.OpenFile(fileName1,os.O_RDONLY|os.O_WRONLY,os.ModePerm)
     //if err != nil{
    
    
     // fmt.Println("err:",err)
     // return
     //}
     //fmt.Println(file4)

     //5关闭文件,
     //file4.Close()

     //6.删除文件或文件夹:
     //删除文件
    //err :=  os.Remove("/Users/ruby/Documents/pro/a/aa.txt")
    //if err != nil{
    
    
    //  fmt.Println("err:",err)
    //  return
    //}
    //fmt.Println("删除文件成功。。")
    //删除目录
    err :=  os.RemoveAll("/Users/ruby/Documents/pro/a/cc")
    if err != nil{
    
    
        fmt.Println("err:",err)
        return
    }
    fmt.Println("删除目录成功。。")
}

I/O操作

I/O操作也叫输入输出操作。其中I是指Input,O是指Output,用于读或者写数据的,有些语言中也叫流操作,是指数据通信的通道。

Golang 标准库对 IO 的抽象非常精巧,各个组件可以随意组合,可以作为接口设计的典范。

一、io包
io包中提供I/O原始操作的一系列接口。它主要包装了一些已有的实现,如 os 包中的那些,并将这些抽象成为实用性的功能和一些其他相关的接口。

由于这些接口和原始的操作以不同的实现包装了低级操作,客户不应假定它们对于并行执行是安全的。

在io包中最重要的是两个接口:Reader和Writer接口,首先来介绍这读的操作.

Reader接口的定义,Read()方法用于读取数据。

type Reader interface {
    
    
        Read(p []byte) (n int, err error)
}
Read 将 len(p) 个字节读取到 p 中。它返回读取的字节数 n(0 <= n <= len(p))以及任何遇到的错误。即使 Read 返回的 n < len(p),它也会在调用过程中使用 p的全部作为暂存空间。若一些数据可用但不到 len(p) 个字节,Read 会照例返回可用的东西,而不是等待更多。

当 Read 在成功读取 n > 0 个字节后遇到一个错误或 EOF 情况,它就会返回读取的字节数。它会从相同的调用中返回(非nil的)错误或从随后的调用中返回错误(和 n == 0)。这种一般情况的一个例子就是 Reader 在输入流结束时会返回一个非零的字节数,可能的返回不是 err == EOF 就是 err == nil。无论如何,下一个 Read 都应当返回 0, EOF。

调用者应当总在考虑到错误 err 前处理 n > 0 的字节。这样做可以在读取一些字节,以及允许的 EOF 行为后正确地处理I/O错误。

Read 的实现会阻止返回零字节的计数和一个 nil 错误,调用者应将这种情况视作空操作。

ReaderFrom接口的定义,封装了基本的 ReadFrom 方法。

type ReaderFrom interface {
    
    
        ReadFrom(r Reader) (n int64, err error)
}
ReadFrom 从 r 中读取数据到对象的数据流中
    直到 r 返回 EOF 或 r 出现读取错误为止
    返回值 n 是读取的字节数
    返回值 err 就是 r 的返回值 err

定义ReaderAt接口,ReaderAt 接口封装了基本的 ReadAt 方法

type ReaderAt interface {
    
    
        ReadAt(p []byte, off int64) (n int, err error)
}
ReadAt 从对象数据流的 off 处读出数据到 p 中
    忽略数据的读写指针,从数据的起始位置偏移 off 处开始读取
    如果对象的数据流只有部分可用,不足以填满 p
    则 ReadAt 将等待所有数据可用之后,继续向 p 中写入
    直到将 p 填满后再返回
    在这点上 ReadAt 要比 Read 更严格
    返回读取的字节数 n 和读取时遇到的错误
    如果 n < len(p),则需要返回一个 err 值来说明
    为什么没有将 p 填满(比如 EOF)
    如果 n = len(p),而且对象的数据没有全部读完,则
    err 将返回 nil
    如果 n = len(p),而且对象的数据刚好全部读完,则
    err 将返回 EOF 或者 nil(不确定)

二、文件读取
file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现。

func (f *File) Read(b []byte) (n int, err error)
//Read方法从f中读取最多len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取0个字节且返回值err为io.EOF。

func (f *File) ReadAt(b []byte, off int64) (n int, err error)
//ReadAt从指定的位置(相对于文件开始位置)读取len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。当n小于len(b)时,本方法总是会返回错误;如果是因为到达文件结尾,返回值err会是io.EOF。

三、实例代码
读取文件中的数据:

package main

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

func main() {
    
    
    /*
    读取数据:
        Reader接口:
            Read(p []byte)(n int, error)
     */
     //读取本地aa.txt文件中的数据
     //step1:打开文件
     fileName := "/Users/ruby/Documents/pro/a/aa.txt"
     file,err := os.Open(fileName)
     if err != nil{
    
    
        fmt.Println("err:",err)
        return
     }
     //step3:关闭文件
     defer file.Close()

     //step2:读取数据
     bs := make([]byte,4,4)
     /*
     //第一次读取
     n,err :=file.Read(bs)
     fmt.Println(err) //
     fmt.Println(n) //4
     fmt.Println(bs) //[97 98 99 100]
    fmt.Println(string(bs)) //abcd

    //第二次读取
    n,err = file.Read(bs)
    fmt.Println(err)//
    fmt.Println(n)//4
    fmt.Println(bs) //[101 102 103 104]
    fmt.Println(string(bs)) //efgh

    //第三次读取
    n,err = file.Read(bs)
    fmt.Println(err) //
    fmt.Println(n) //2
    fmt.Println(bs) //[105 106 103 104]
    fmt.Println(string(bs)) //ijgh

    //第四次读取
    n,err = file.Read(bs)
    fmt.Println(err) //EOF
    fmt.Println(n) //0
     */
     n := -1
     for{
    
    
        n,err = file.Read(bs)
        if n == 0 || err == io.EOF{
    
    
            fmt.Println("读取到了文件的末尾,结束读取操作。。")
            break
        }
        fmt.Println(string(bs[:n]))
     }
}

I/O写操作

一、io包中的write
Writer接口的定义,Write()方法用于写出数据。

type Writer interface {
    
    
        Write(p []byte) (n int, err error)
}

Write 将 len§ 个字节从 p 中写入到基本数据流中。它返回从 p 中被写入的字节数n(0 <= n <=
len§)以及任何遇到的引起写入提前停止的错误。若 Write 返回的n < len§,它就必须返回一个非nil的错误。Write
不能修改此切片的数据,即便它是临时的。

Seeker接口的定义,封装了基本的 Seek 方法。

type Seeker interface {
    
    
        Seek(offset int64, whence int) (int64, error)
}

Seeker 用来移动数据的读写指针
Seek 设置下一次读写操作的指针位置,每次的读写操作都是从指针位置开始的
whence 的含义:
如果 whence 为 0:表示从数据的开头开始移动指针
如果 whence 为 1:表示从数据的当前指针位置开始移动指针
如果 whence 为 2:表示从数据的尾部开始移动指针
offset 是指针移动的偏移量
返回移动后的指针位置和移动过程中遇到的任何错误

WriterTo接口的定义,封装了基本的 WriteTo 方法。

type WriterTo interface {
    
    
        WriteTo(w Writer) (n int64, err error)
}

WriterTo 将对象的数据流写入到 w 中
直到对象的数据流全部写入完毕或遇到写入错误为止
返回值 n 是写入的字节数
返回值 err 就是 w 的返回值 err

定义WriterAt接口,WriterAt 接口封装了基本的 WriteAt 方法

type WriterAt interface {
    
    
        WriteAt(p []byte, off int64) (n int, err error)
}

WriteAt 将 p 中的数据写入到对象数据流的 off 处
忽略数据的读写指针,从数据的起始位置偏移 off 处开始写入
返回写入的字节数和写入时遇到的错误
如果 n < len§,则必须返回一个 err 值来说明
为什么没有将 p 完全写入

二、文件写出
file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现


func (f *File) Write(b []byte) (n int, err error)
//Write向文件中写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len(b),本方法会返回一个非nil的错误。

func (f *File) WriteString(s string) (ret int, err error)
//WriteString类似Write,但接受一个字符串参数。

func (f *File) WriteAt(b []byte, off int64) (n int, err error)
//WriteAt在指定的位置(相对于文件开始位置)写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len(b),本方法会返回一个非nil的错误。

func (f *File) Seek(offset int64, whence int) (ret int64, err error)
//Seek设置下一次读/写的位置。offset为相对偏移量,而whence决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。它返回新的偏移量(相对开头)和可能的错误。

func (f *File) Sync() (err error)
//Sync递交文件的当前内容进行稳定的存储。一般来说,这表示将文件系统的最近写入的数据在内存中的拷贝刷新到硬盘中稳定保存。

三、实例代码
写出数据到本地文件:

package main

import (
    "os"
    "fmt"
    "log"
)

func main() {
    
    
    /*
    写出数据:
     */

     fileName := "/Users/ruby/Documents/pro/a/ab.txt"
    //step1:打开文件
    //step2:写出数据
    //step3:关闭文件
    //file,err := os.Open(fileName)
    file,err := os.OpenFile(fileName,os.O_CREATE|os.O_WRONLY|os.O_APPEND,os.ModePerm)
    if err != nil{
    
    
        fmt.Println(err)
        return
    }
    defer file.Close()

    //写出数据
    //bs :=[]byte{
    
    65,66,67,68,69,70}//A,B,C,D,E,F
    bs :=[] byte{
    
    97,98,99,100} //a,b,c,d
    //n,err := file.Write(bs)
    n,err := file.Write(bs[:2])
    fmt.Println(n)
    HandleErr(err)
    file.WriteString("\n")

    //直接写出字符串
    n,err = file.WriteString("HelloWorld")
    fmt.Println(n)
    HandleErr(err)

    file.WriteString("\n")
    n,err =file.Write([]byte("today"))
    fmt.Println(n)
    HandleErr(err)

}

func HandleErr(err error){
    
    
    if err != nil{
    
    
        log.Fatal(err)
    }
}

文件复制

第004节:文件复制
Golang中国 • 2019-08-23 • 阅读 1490
在io包中主要是操作流的一些方法,今天主要学习一下copy。就是把一个文件复制到另一个目录下。

它的原理就是通过程序,从源文件读取文件中的数据,在写出到目标文件里。
在这里插入图片描述
一、方法一:io包下的Read()和Write()方法实现
我们可以通过io包下的Read()和Write()方法,边读边写,就能够实现文件的复制。这个方法是按块读取文件,块的大小也会影响到程序的性能。

}
/*
该函数的功能:实现文件的拷贝,返回值是拷贝的总数量(字节),错误
 */
func copyFile1(srcFile,destFile string)(int,error){
    
    
    file1,err:=os.Open(srcFile)
    if err != nil{
    
    
        return 0,err
    }
    file2,err:=os.OpenFile(destFile,os.O_WRONLY|os.O_CREATE,os.ModePerm)
    if err !=nil{
    
    
        return 0,err
    }
    defer file1.Close()
    defer file2.Close()
    //拷贝数据
    bs := make([]byte,1024,1024)
    n :=-1//读取的数据量
    total := 0
    for {
    
    
        n,err = file1.Read(bs)
        if err == io.EOF || n == 0{
    
    
            fmt.Println("拷贝完毕。。")
            break
        }else if err !=nil{
    
    
            fmt.Println("报错了。。。")
            return total,err
        }
        total += n
        file2.Write(bs[:n])
    }
    return total,nil

}

二、方法二:io包下的Copy()方法实现
我们也可以直接使用io包下的Copy()方法。

示例代码如下:

func copyFile2(srcFile, destFile string)(int64,error){
    
    
    file1,err:=os.Open(srcFile)
    if err != nil{
    
    
        return 0,err
    }
    file2,err:=os.OpenFile(destFile,os.O_WRONLY|os.O_CREATE,os.ModePerm)
    if err !=nil{
    
    
        return 0,err
    }
    defer file1.Close()
    defer file2.Close()

    return io.Copy(file2,file1)
}

扩展内容:
在io包(golang 版本 1.12)中,不止提供了Copy()方法,还有另外2个公开的copy方法:CopyN(),CopyBuffer()。

Copy(dst,src) 为复制src 全部到 dst 中。

CopyN(dst,src,n) 为复制src 中 n 个字节到 dst。

CopyBuffer(dst,src,buf)为指定一个buf缓存区,以这个大小完全复制。

在这里插入图片描述
无论是哪个copy方法最终都是由copyBuffer()这个私有方法实现的

func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
    
    
    // If the reader has a WriteTo method, use it to do the copy.
    // Avoids an allocation and a copy.
    if wt, ok := src.(WriterTo); ok {
    
    
        return wt.WriteTo(dst)
    }
    // Similarly, if the writer has a ReadFrom method, use it to do the copy.
    if rt, ok := dst.(ReaderFrom); ok {
    
    
        return rt.ReadFrom(src)
    }
    if buf == nil {
    
    
        size := 32 * 1024
        if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
    
    
            if l.N < 1 {
    
    
                size = 1
            } else {
    
    
                size = int(l.N)
            }
        }
        buf = make([]byte, size)
    }
    for {
    
    
        nr, er := src.Read(buf)
        if nr > 0 {
    
    
            nw, ew := dst.Write(buf[0:nr])
            if nw > 0 {
    
    
                written += int64(nw)
            }
            if ew != nil {
    
    
                err = ew
                break
            }
            if nr != nw {
    
    
                err = ErrShortWrite
                break
            }
        }
        if er != nil {
    
    
            if er != EOF {
    
    
                err = er
            }
            break
        }
    }
    return written, err
}

从这部分代码可以看出,复制主要分为3种。

1.如果被复制的Reader(src)会尝试能否断言成writerTo,如果可以则直接调用下面的writerTo方法

2.如果 Writer(dst) 会尝试能否断言成ReadFrom ,如果可以则直接调用下面的readfrom方法

3.如果都木有实现,则调用底层read实现复制。

其中,有这么一段代码:

if buf == nil {
    
    
        size := 32 * 1024
        if l, ok := src.(*LimitedReader); ok && int64(size) > l.N {
    
    
            if l.N < 1 {
    
    
                size = 1
            } else {
    
    
                size = int(l.N)
            }
        }
        buf = make([]byte, size)
    }

这部分主要是实现了对Copy和CopyN的处理。通过上面的调用关系图,我们看出CopyN在调用后,会把Reader转成LimiteReader。

区别是如果Copy,直接建立一个缓存区默认大小为 32* 1024 的buf,如果是CopyN 会先判断 要复制的字节数,如果小于默认大小,会创建一个等于要复制字节数的buf。

三、方法三:ioutil包
第三种方法是使用ioutil包中的 ioutil.WriteFile()和 ioutil.ReadFile(),但由于使用一次性读取文件,再一次性写入文件的方式,所以该方法不适用于大文件,容易内存溢出。

示例代码:


func copyFile3(srcFile, destFile string)(int,error){
    
    
    input, err := ioutil.ReadFile(srcFile)
    if err != nil {
    
    
        fmt.Println(err)
        return 0,err
    }

    err = ioutil.WriteFile(destFile, input, 0644)
    if err != nil {
    
    
        fmt.Println("操作失败:", destFile)
        fmt.Println(err)
        return 0,err
    }

    return len(input),nil
}

代码:

func main() {
    
    
    /*
    复制文件:
     */
    //srcFile := "/home/ruby/文档/pro/aa.txt"
    //destFile := "/home/ruby/文档/aa.txt"

    srcFile :="/Users/ruby/Documents/pro/a/001_小程序入门.mp4"
    destFile:="001_小程序入门.mp4"
    total,err:=copyFile1(srcFile,destFile)
    fmt.Println(err)
    fmt.Println(total)

}

第一种:io包下Read()和Write()直接读写:我们自己创建读取数据的切片的大小,直接影响性能。

localhost:l_file ruby$ time go run demo05_copy.go 
拷贝完毕。。

401386819

real    0m7.911s
user    0m2.900s
sys     0m7.661s

第二种:io包下Copy()方法:

localhost:l_file ruby$ time go run demo05_copy.go 

401386819

real    0m1.594s
user    0m0.533s
sys     0m1.136s
COPY

第三种:ioutil包

localhost:l_file ruby$ time go run demo05_copy.go 

401386819

real    0m1.515s
user    0m0.339s
sys     0m0.625s

端点续传

一、Seeker接口
Seeker是包装基本Seek方法的接口。

type Seeker interface {
    
    
        Seek(offset int64, whence int) (int64, error)
}

seek(offset,whence),设置指针光标的位置,随机读写文件:

​ 第一个参数:偏移量
​ 第二个参数:如何设置

​ 0:seekStart表示相对于文件开始,
​ 1:seekCurrent表示相对于当前偏移量,
​ 2:seek end表示相对于结束。

const (
    SeekStart   = 0 // seek relative to the origin of the file
    SeekCurrent = 1 // seek relative to the current offset
    SeekEnd     = 2 // seek relative to the end
)

示例代码:

我们要读取本地/Users/ruby/Documents/pro/a 目录下的aa.txt文件,文件中的内容是:abcdefghij这几个字符。

package main

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

func main() {
    
    
    /*
    seek(offset,whence),设置指针光标的位置
    第一个参数:偏移量
    第二个参数:如何设置
        0:seekStart表示相对于文件开始,
        1:seekCurrent表示相对于当前偏移量,
        2:seek end表示相对于结束。

    const (
    SeekStart   = 0 // seek relative to the origin of the file
    SeekCurrent = 1 // seek relative to the current offset
    SeekEnd     = 2 // seek relative to the end
)

    随机读取文件:
        可以设置指针光标的位置
     */

    file,_:=os.OpenFile("/Users/ruby/Documents/pro/a/aa.txt",os.O_RDWR,0)
    defer file.Close()
    bs :=[]byte{
    
    0}

    file.Read(bs)
    fmt.Println(string(bs))

    file.Seek(4,io.SeekStart)
    file.Read(bs)
    fmt.Println(string(bs))
    file.Seek(2,0) //也是SeekStart
    file.Read(bs)
    fmt.Println(string(bs))

    file.Seek(3,io.SeekCurrent)
    file.Read(bs)
    fmt.Println(string(bs))

    file.Seek(0,io.SeekEnd)
    file.WriteString("ABC")
}

二、断点续传
首先思考几个问题
Q1:如果你要传的文件,比较大,那么是否有方法可以缩短耗时?
Q2:如果在文件传递过程中,程序因各种原因被迫中断了,那么下次再重启时,文件是否还需要重头开始?
Q3:传递文件的时候,支持暂停和恢复么?即使这两个操作分布在程序进程被杀前后。

通过断点续传可以实现,不同的语言有不同的实现方式。我们看看Go语言中,通过Seek()方法如何实现:

先说一下思路:想实现断点续传,主要就是记住上一次已经传递了多少数据,那我们可以创建一个临时文件,记录已经传递的数据量,当恢复传递的时候,先从临时文件中读取上次已经传递的数据量,然后通过Seek()方法,设置到该读和该写的位置,再继续传递数据。

示例代码:

package main

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

func main() {
    
    
    /*
    断点续传:
        文件传递:文件复制
            /Users/ruby/Documents/pro/a/guliang.jpeg

        复制到
            guliang4.jpeg

    思路:
        边复制,边记录复制的总量
     */

    srcFile:="/Users/ruby/Documents/pro/a/guliang.jpeg"
    destFile:="guliang4.jpeg"
    tempFile:=destFile+"temp.txt"
    //fmt.Println(tempFile)
    file1,_:=os.Open(srcFile)
    file2,_:=os.OpenFile(destFile,os.O_CREATE|os.O_WRONLY,os.ModePerm)
    file3,_:=os.OpenFile(tempFile,os.O_CREATE|os.O_RDWR,os.ModePerm)

    defer file1.Close()
    defer file2.Close()
    //1.读取临时文件中的数据,根据seek
    file3.Seek(0,io.SeekStart)
    bs:=make([]byte,100,100)
    n1,err:=file3.Read(bs)
    fmt.Println(n1)
    countStr:=string(bs[:n1])
    fmt.Println(countStr)
    //count,_:=strconv.Atoi(countStr)
    count,_:=strconv.ParseInt(countStr,10,64)
    fmt.Println(count)

    //2. 设置读,写的偏移量
    file1.Seek(count,0)
    file2.Seek(count,0)
    data:=make([]byte,1024,1024)
    n2:=-1// 读取的数据量
    n3:=-1//写出的数据量
    total :=int(count)//读取的总量

    for{
    
    
        //3.读取数据
        n2,err=file1.Read(data)
        if err ==io.EOF{
    
    
            fmt.Println("文件复制完毕。。")
            file3.Close()
            os.Remove(tempFile)
            break
        }
        //将数据写入到目标文件
        n3,_=file2.Write(data[:n2])
        total += n3
        //将复制总量,存储到临时文件中
        file3.Seek(0,io.SeekStart)
        file3.WriteString(strconv.Itoa(total))

        //假装断电
        //if total>8000{
    
    
        //  panic("假装断电了。。。,假装的。。。")
        //}
    }

}

bufio包

一、bufio包原理
bufio 是通过缓冲来提高效率。

io操作本身的效率并不低,低的是频繁的访问本地磁盘的文件。所以bufio就提供了缓冲区(分配一块内存),读和写都先在缓冲区中,最后再读写文件,来降低访问本地磁盘的次数,从而提高效率。

简单的说就是,把文件读取进缓冲(内存)之后再读取的时候就可以避免文件系统的io 从而提高速度。同理,在进行写操作时,先把文件写入缓冲(内存),然后由缓冲写入文件系统。看完以上解释有人可能会表示困惑了,直接把 内容->文件 和 内容->缓冲->文件相比, 缓冲区好像没有起到作用嘛。其实缓冲区的设计是为了存储多次的写入,最后一口气把缓冲区内容写入文件。
在这里插入图片描述
bufio 封装了io.Reader或io.Writer接口对象,并创建另一个也实现了该接口的对象。

io.Reader或io.Writer 接口实现read() 和 write() 方法,对于实现这个接口的对象都是可以使用这两个方法的。

Reader对象

bufio.Reader 是bufio中对io.Reader 的封装

// Reader implements buffering for an io.Reader object.
type Reader struct {
    
    
    buf          []byte
    rd           io.Reader // reader provided by the client
    r, w         int       // buf read and write positions
    err          error
    lastByte     int // last byte read for UnreadByte; -1 means invalid
    lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}

bufio.Read(p []byte) 相当于读取大小len§的内容,思路如下:

当缓存区有内容的时,将缓存区内容全部填入p并清空缓存区
当缓存区没有内容的时候且len§>len(buf),即要读取的内容比缓存区还要大,直接去文件读取即可
当缓存区没有内容的时候且len§<len(buf),即要读取的内容比缓存区小,缓存区从文件读取内容充满缓存区,并将p填满(此时缓存区有剩余内容)
以后再次读取时缓存区有内容,将缓存区内容全部填入p并清空缓存区(此时和情况1一样)
源码:

// Read reads data into p.
// It returns the number of bytes read into p.
// The bytes are taken from at most one Read on the underlying Reader,
// hence n may be less than len(p).
// To read exactly len(p) bytes, use io.ReadFull(b, p).
// At EOF, the count will be zero and err will be io.EOF.
func (b *Reader) Read(p []byte) (n int, err error) {
    
    
    n = len(p)
    if n == 0 {
    
    
        return 0, b.readErr()
    }
    if b.r == b.w {
    
    
        if b.err != nil {
    
    
            return 0, b.readErr()
        }
        if len(p) >= len(b.buf) {
    
    
            // Large read, empty buffer.
            // Read directly into p to avoid copy.
            n, b.err = b.rd.Read(p)
            if n < 0 {
    
    
                panic(errNegativeRead)
            }
            if n > 0 {
    
    
                b.lastByte = int(p[n-1])
                b.lastRuneSize = -1
            }
            return n, b.readErr()
        }
        // One read.
        // Do not use b.fill, which will loop.
        b.r = 0
        b.w = 0
        n, b.err = b.rd.Read(b.buf)
        if n < 0 {
    
    
            panic(errNegativeRead)
        }
        if n == 0 {
    
    
            return 0, b.readErr()
        }
        b.w += n
    }

    // copy as much as we can
    n = copy(p, b.buf[b.r:b.w])
    b.r += n
    b.lastByte = int(b.buf[b.r-1])
    b.lastRuneSize = -1
    return n, nil
}

说明:

reader内部通过维护一个r, w 即读入和写入的位置索引来判断是否缓存区内容被全部读出。

Writer对象

bufio.Writer 是bufio中对io.Writer 的封装

// Writer implements buffering for an io.Writer object.
// If an error occurs writing to a Writer, no more data will be
// accepted and all subsequent writes, and Flush, will return the error.
// After all data has been written, the client should call the
// Flush method to guarantee all data has been forwarded to
// the underlying io.Writer.
type Writer struct {
    
    
    err error
    buf []byte
    n   int
    wr  io.Writer
}

bufio.Write(p []byte) 的思路如下

判断buf中可用容量是否可以放下 p
如果能放下,直接把p拼接到buf后面,即把内容放到缓冲区
如果缓冲区的可用容量不足以放下,且此时缓冲区是空的,直接把p写入文件即可
如果缓冲区的可用容量不足以放下,且此时缓冲区有内容,则用p把缓冲区填满,把缓冲区所有内容写入文件,并清空缓冲区
判断p的剩余内容大小能否放到缓冲区,如果能放下(此时和步骤1情况一样)则把内容放到缓冲区
如果p的剩余内容依旧大于缓冲区,(注意此时缓冲区是空的,情况和步骤3一样)则把p的剩余内容直接写入文件
以下是源码

// Write writes the contents of p into the buffer.
// It returns the number of bytes written.
// If nn < len(p), it also returns an error explaining
// why the write is short.
func (b *Writer) Write(p []byte) (nn int, err error) {
    
    
    for len(p) > b.Available() && b.err == nil {
    
    
        var n int
        if b.Buffered() == 0 {
    
    
            // Large write, empty buffer.
            // Write directly from p to avoid copy.
            n, b.err = b.wr.Write(p)
        } else {
    
    
            n = copy(b.buf[b.n:], p)
            b.n += n
            b.Flush()
        }
        nn += n
        p = p[n:]
    }
    if b.err != nil {
    
    
        return nn, b.err
    }
    n := copy(b.buf[b.n:], p)
    b.n += n
    nn += n
    return nn, nil
}

说明:

b.wr 存储的是一个io.writer对象,实现了Write()的接口,所以可以使用b.wr.Write§ 将p的内容写入文件。

b.flush() 会将缓存区内容写入文件,当所有写入完成后,因为缓存区会存储内容,所以需要手动flush()到文件。

b.Available() 为buf可用容量,等于len(buf) - n。

下图解释的是其中一种情况,即缓存区有内容,剩余p大于缓存区
在这里插入图片描述
二、bufio包
bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。

bufio.Reader:

bufio.Reader 实现了如下接口:
io.Reader
io.WriterTo
io.ByteScanner
io.RuneScanner

// NewReaderSize 将 rd 封装成一个带缓存的 bufio.Reader 对象,
// 缓存大小由 size 指定(如果小于 16 则会被设置为 16)。
// 如果 rd 的基类型就是有足够缓存的 bufio.Reader 类型,则直接将
// rd 转换为基类型返回。
func NewReaderSize(rd io.Reader, size int) *Reader

// NewReader 相当于 NewReaderSize(rd, 4096)
func NewReader(rd io.Reader) *Reader

// Peek 返回缓存的一个切片,该切片引用缓存中前 n 个字节的数据,
// 该操作不会将数据读出,只是引用,引用的数据在下一次读取操作之
// 前是有效的。如果切片长度小于 n,则返回一个错误信息说明原因。
// 如果 n 大于缓存的总大小,则返回 ErrBufferFull。
func (b *Reader) Peek(n int) ([]byte, error)

// Read 从 b 中读出数据到 p 中,返回读出的字节数和遇到的错误。
// 如果缓存不为空,则只能读出缓存中的数据,不会从底层 io.Reader
// 中提取数据,如果缓存为空,则:
// 1、len(p) >= 缓存大小,则跳过缓存,直接从底层 io.Reader 中读
// 出到 p 中。
// 2、len(p) < 缓存大小,则先将数据从底层 io.Reader 中读取到缓存
// 中,再从缓存读取到 p 中。
func (b *Reader) Read(p []byte) (n int, err error)

// Buffered 返回缓存中未读取的数据的长度。
func (b *Reader) Buffered() int

// ReadBytes 功能同 ReadSlice,只不过返回的是缓存的拷贝。
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)

// ReadString 功能同 ReadBytes,只不过返回的是字符串。
func (b *Reader) ReadString(delim byte) (line string, err error)

...

bufio.Writer:

bufio.Writer 实现了如下接口:
io.Writer
io.ReaderFrom
io.ByteWriter

// NewWriterSize 将 wr 封装成一个带缓存的 bufio.Writer 对象,
// 缓存大小由 size 指定(如果小于 4096 则会被设置为 4096)。
// 如果 wr 的基类型就是有足够缓存的 bufio.Writer 类型,则直接将
// wr 转换为基类型返回。
func NewWriterSize(wr io.Writer, size int) *Writer

// NewWriter 相当于 NewWriterSize(wr, 4096)
func NewWriter(wr io.Writer) *Writer

// WriteString 功能同 Write,只不过写入的是字符串
func (b *Writer) WriteString(s string) (int, error)

// WriteRune 向 b 写入 r 的 UTF-8 编码,返回 r 的编码长度。
func (b *Writer) WriteRune(r rune) (size int, err error)

// Flush 将缓存中的数据提交到底层的 io.Writer 中
func (b *Writer) Flush() error

// Available 返回缓存中未使用的空间的长度
func (b *Writer) Available() int

// Buffered 返回缓存中未提交的数据的长度
func (b *Writer) Buffered() int

// Reset 将 b 的底层 Writer 重新指定为 w,同时丢弃缓存中的所有数据,复位
// 所有标记和错误信息。相当于创建了一个新的 bufio.Writer。
func (b *Writer) Reset(w io.Writer)

...

三、实例代码
读取数据:

package main

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

func main() {
    
    
    /*
    bufio:高效io读写
        buffer缓存
        io:input/output

    将io包下的Reader,Write对象进行包装,带缓存的包装,提高读写的效率

        ReadBytes()
        ReadString()
        ReadLine()

     */

     fileName:="/Users/ruby/Documents/pro/a/english.txt"
     file,err := os.Open(fileName)
     if err != nil{
    
    
        fmt.Println(err)
        return
     }
     defer file.Close()

     //创建Reader对象
     //b1 := bufio.NewReader(file)
     //1.Read(),高效读取
     //p := make([]byte,1024)
     //n1,err := b1.Read(p)
     //fmt.Println(n1)
     //fmt.Println(string(p[:n1]))

     //2.ReadLine()
     //data,flag,err := b1.ReadLine()
     //fmt.Println(flag)
     //fmt.Println(err)
     //fmt.Println(data)
     //fmt.Println(string(data))

     //3.ReadString()
    // s1,err :=b1.ReadString('\n')
    // fmt.Println(err)
    // fmt.Println(s1)
    //
    // s1,err = b1.ReadString('\n')
    // fmt.Println(err)
    // fmt.Println(s1)
    //
    //s1,err = b1.ReadString('\n')
    //fmt.Println(err)
    //fmt.Println(s1)
    //
    //for{
    
    
    //  s1,err := b1.ReadString('\n')
    //  if err == io.EOF{
    
    
    //      fmt.Println("读取完毕。。")
    //      break
    //  }
    //  fmt.Println(s1)
    //}

    //4.ReadBytes()
    //data,err :=b1.ReadBytes('\n')
    //fmt.Println(err)
    //fmt.Println(string(data))

    //Scanner
    //s2 := ""
    //fmt.Scanln(&s2)
    //fmt.Println(s2)

    b2 := bufio.NewReader(os.Stdin)
    s2, _ := b2.ReadString('\n')
    fmt.Println(s2)

}

写数据示例代码:

package main

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

func main() {
    
    
    /*
    bufio:高效io读写
        buffer缓存
        io:input/output

    将io包下的Reader,Write对象进行包装,带缓存的包装,提高读写的效率

        func (b *Writer) Write(p []byte) (nn int, err error)
        func (b *Writer) WriteByte(c byte) error
        func (b *Writer) WriteRune(r rune) (size int, err error)
        func (b *Writer) WriteString(s string) (int, error)

     */

     fileName := "/Users/ruby/Documents/pro/a/cc.txt"
     file,err := os.OpenFile(fileName,os.O_CREATE|os.O_WRONLY,os.ModePerm)
     if err != nil{
    
    
        fmt.Println(err)
        return
     }
     defer file.Close()

     w1 := bufio.NewWriter(file)
     //n,err := w1.WriteString("helloworld")
     //fmt.Println(err)
     //fmt.Println(n)
     //w1.Flush() //刷新缓冲区

     for i:=1;i<=1000;i++{
    
    
        w1.WriteString(fmt.Sprintf("%d:hello",i))
     }
     w1.Flush()
}

ioutil包

除了io包可以读写数据,Go语言中还提供了一个辅助的工具包就是ioutil,里面的方法虽然不多,但是都还蛮好用的。

import "io/ioutil"

该包的介绍只有一句话:Package ioutil implements some I/O utility functions。

一、ioutil包的方法
下面我们来看一下里面的方法:

// Discard 是一个 io.Writer 接口,调用它的 Write 方法将不做任何事情
// 并且始终成功返回。
var Discard io.Writer = devNull(0)

// ReadAll 读取 r 中的所有数据,返回读取的数据和遇到的错误。
// 如果读取成功,则 err 返回 nil,而不是 EOF,因为 ReadAll 定义为读取
// 所有数据,所以不会把 EOF 当做错误处理。
func ReadAll(r io.Reader) ([]byte, error)

// ReadFile 读取文件中的所有数据,返回读取的数据和遇到的错误。
// 如果读取成功,则 err 返回 nil,而不是 EOF
func ReadFile(filename string) ([]byte, error)

// WriteFile 向文件中写入数据,写入前会清空文件。
// 如果文件不存在,则会以指定的权限创建该文件。
// 返回遇到的错误。
func WriteFile(filename string, data []byte, perm os.FileMode) error

// ReadDir 读取指定目录中的所有目录和文件(不包括子目录)。
// 返回读取到的文件信息列表和遇到的错误,列表是经过排序的。
func ReadDir(dirname string) ([]os.FileInfo, error)

// NopCloser 将 r 包装为一个 ReadCloser 类型,但 Close 方法不做任何事情。
func NopCloser(r io.Reader) io.ReadCloser

// TempFile 在 dir 目录中创建一个以 prefix 为前缀的临时文件,并将其以读
// 写模式打开。返回创建的文件对象和遇到的错误。
// 如果 dir 为空,则在默认的临时目录中创建文件(参见 os.TempDir),多次
// 调用会创建不同的临时文件,调用者可以通过 f.Name() 获取文件的完整路径。
// 调用本函数所创建的临时文件,应该由调用者自己删除。
func TempFile(dir, prefix string) (f *os.File, err error)

// TempDir 功能同 TempFile,只不过创建的是目录,返回目录的完整路径。
func TempDir(dir, prefix string) (name string, err error)

二、示例代码:

package main

import (
    "io/ioutil"
    "fmt"
    "os"
)

func main() {
    
    
    /*
    ioutil包:
        ReadFile()
        WriteFile()
        ReadDir()
        ..
     */

    //1.读取文件中的所有的数据
    //fileName1 := "/Users/ruby/Documents/pro/a/aa.txt"
    //data, err := ioutil.ReadFile(fileName1)
    //fmt.Println(err)
    //fmt.Println(string(data))

    //2.写出数据
    //fileName2:="/Users/ruby/Documents/pro/a/bbb.txt"
    //s1:="helloworld面朝大海春暖花开"
    //err:=ioutil.WriteFile(fileName2,[]byte(s1),0777)
    //fmt.Println(err)

    //3.
    //s2:="qwertyuiopsdfghjklzxcvbnm"
    //r1:=strings.NewReader(s2)
    //data,_:=ioutil.ReadAll(r1)
    //fmt.Println(data)

    //4.ReadDir(),读取一个目录下的子内容:子文件和子目录,但是仅有一层
    //dirName:="/Users/ruby/Documents/pro/a"
    //fileInfos,_:=ioutil.ReadDir(dirName)
    //fmt.Println(len(fileInfos))
    //for i:=0;i

遍历文件夹

学习io之后,尤其是文件操作,我们就可以遍历给定的目录文件夹了。可以使用ioutil包下的readDir()方法,这个方法可以获取指定目录下的内容,返回文件和子目录。

因为文件夹下还有子文件夹,而ioutil包的ReadDir()只能获取一层目录,所以我们需要自己去设计算法来实现,最容易实现的思路就是使用递归。

示例代码:

package main

import (
    "io/ioutil"
    "fmt"
    "log"
)

func main() {
    
    
    /**
    遍历文件夹:
     */

    dirname := "/Users/ruby/Documents/pro"
    listFiles(dirname, 0)

}

func listFiles(dirname string, level int) {
    
    
    // level用来记录当前递归的层次
    // 生成有层次感的空格
    s := "|--"
    for i := 0; i < level; i++ {
    
    
        s = "|   " + s
    }

    fileInfos, err := ioutil.ReadDir(dirname)
    if err != nil{
    
    
        log.Fatal(err)
    }
    for _, fi := range fileInfos {
    
    
        filename := dirname + "/" + fi.Name()
        fmt.Printf("%s%s\n", s, filename)
        if fi.IsDir() {
    
    
            //继续遍历fi这个目录
            listFiles(filename, level+1)
        }
    }
}

并发goruntine,channel

并发性Concurrency

1.1 多任务
怎么来理解多任务呢?其实就是指我们的操作系统可以同时执行多个任务。举个例子,你一边听音乐,一边刷微博,一边聊QQ,一边用Markdown写作业,这就是多任务,至少同时有4个任务正在运行。还有很多任务悄悄地在后台同时运行着,只是界面上没有显示而已。
1.2 什么是并发
Go是并发语言,而不是并行语言。在讨论如何在Go中进行并发处理之前,我们首先必须了解什么是并发,以及它与并行性有什么不同。(Go is a concurrent language and not a parallel one. )

并发性Concurrency是同时处理许多事情的能力。

举个例子,假设一个人在晨跑。在晨跑时,他的鞋带松了。现在这个人停止跑步,系鞋带,然后又开始跑步。这是一个典型的并发性示例。这个人能够同时处理跑步和系鞋带,这是一个人能够同时处理很多事情。

什么是并行性parallelism,它与并发concurrency有什么不同?
并行就是同时做很多事情。这听起来可能与并发类似,但实际上是不同的。

让我们用同样的慢跑例子更好地理解它。在这种情况下,我们假设这个人正在慢跑,并且使用它的手机听音乐。在这种情况下,一个人一边慢跑一边听音乐,那就是他同时在做很多事情。这就是所谓的并行性(parallelism)。

并发性和并行性——一种技术上的观点。
假设我们正在编写一个web浏览器。web浏览器有各种组件。其中两个是web页面呈现区域和下载文件从internet下载的下载器。假设我们以这样的方式构建了浏览器的代码,这样每个组件都可以独立地执行。当这个浏览器运行在单个核处理器中时,处理器将在浏览器的两个组件之间进行上下文切换。它可能会下载一个文件一段时间,然后它可能会切换到呈现用户请求的网页的html。这就是所谓的并发性。并发进程从不同的时间点开始,它们的执行周期重叠。在这种情况下,下载和呈现从不同的时间点开始,它们的执行重叠。

假设同一浏览器运行在多核处理器上。在这种情况下,文件下载组件和HTML呈现组件可能同时在不同的内核中运行。这就是所谓的并行性。
在这里插入图片描述
并行性Parallelism不会总是导致更快的执行时间。这是因为并行运行的组件可能需要相互通信。例如,在我们的浏览器中,当文件下载完成时,应该将其传递给用户,比如使用弹出窗口。这种通信发生在负责下载的组件和负责呈现用户界面的组件之间。这种通信开销在并发concurrent 系统中很低。当组件在多个内核中并行concurrent 运行时,这种通信开销很高。因此,并行程序并不总是导致更快的执行时间!
在这里插入图片描述
1.3 进程、线程、协程
进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

进程
进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。
进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。

线程
线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。
线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

协程
协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。
子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

协程与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

Go语言对于并发的实现是靠协程,Goroutine

Go语言的协程——Goroutine

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

进程
进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。
进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。

线程
线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。
线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

协程
协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。
子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

协程的特点在于是一个线程执行,与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

协程的特点在于是一个线程执行,与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

Goroutines在线程上的优势。

与线程相比,Goroutines非常便宜。它们只是堆栈大小的几个kb,堆栈可以根据应用程序的需要增长和收缩,而在线程的情况下,堆栈大小必须指定并且是固定的
Goroutines被多路复用到较少的OS线程。在一个程序中可能只有一个线程与数千个Goroutines。如果线程中的任何Goroutine都表示等待用户输入,则会创建另一个OS线程,剩下的Goroutines被转移到新的OS线程。所有这些都由运行时进行处理,我们作为程序员从这些复杂的细节中抽象出来,并得到了一个与并发工作相关的干净的API。
当使用Goroutines访问共享内存时,通过设计的通道可以防止竞态条件发生。通道可以被认为是Goroutines通信的管道。

1.2 主goroutine
封装main函数的goroutine称为主goroutine。

主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随后,这个go程序的运行也会终止。

此后,主goroutine会进行一系列的初始化工作,涉及的工作内容大致如下:

创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束

启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识

执行mian包中的init函数

执行main函数

执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。最后主goroutine会结束自己以及当前进程的运行。

1.3 如何使用Goroutines
在函数或方法调用前面加上关键字go,您将会同时运行一个新的Goroutine。

实例代码:

package main

import (  
    "fmt"
)

func hello() {
    
      
    fmt.Println("Hello world goroutine")
}
func main() {
    
      
    go hello()
    fmt.Println("main function")
}

我们开始的Goroutine怎么样了?我们需要了解Goroutine的规则

当新的Goroutine开始时,Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束。当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码。
main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。
修改以上代码:

package main

import (  
    "fmt"
    "time"
)

func hello() {
    
      
    fmt.Println("Hello world goroutine")
}
func main() {
    
      
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

在上面的程序中,我们已经调用了时间包的Sleep方法,它会在执行过程中睡觉。在这种情况下,main的goroutine被用来睡觉1秒。现在调用go hello()有足够的时间在main Goroutine终止之前执行。这个程序首先打印Hello world goroutine,等待1秒,然后打印main函数。
1.4 启动多个Goroutines
示例代码:

package main

import (  
    "fmt"
    "time"
)

func numbers() {
    
      
    for i := 1; i <= 5; i++ {
    
    
        time.Sleep(250 * time.Millisecond)
        fmt.Printf("%d ", i)
    }
}
func alphabets() {
    
      
    for i := 'a'; i <= 'e'; i++ {
    
    
        time.Sleep(400 * time.Millisecond)
        fmt.Printf("%c ", i)
    }
}
func main() {
    
      
    go numbers()
    go alphabets()
    time.Sleep(3000 * time.Millisecond)
    fmt.Println("main terminated")
}

在这里插入图片描述

go语言的并发模型

Go 语言相比Java等一个很大的优势就是可以方便地编写并发程序。Go 语言内置了 goroutine 机制,使用goroutine可以快速地开发并发程序, 更好的利用多核处理器资源。接下来我们来了解一下Go语言的并发原理。

一、线程模型
在现代操作系统中,线程是处理器调度和分配的基本单位,进程则作为资源拥有的基本单位。每个进程是由私有的虚拟地址空间、代码、数据和其它各种系统资源组成。线程是进程内部的一个执行单元。 每一个进程至少有一个主执行线程,它无需由用户去主动创建,是由系统自动创建的。 用户根据需要在应用程序中创建其它线程,多个线程并发地运行于同一个进程中。

我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。

我们现在的计算机语言,可以狭义的认为是一种“软件”,它们中所谓的“线程”,往往是用户态的线程,和操作系统本身内核态的线程(简称KSE),还是有区别的。

Go并发编程模型在底层是由操作系统所提供的线程库支撑的,因此还是得从线程实现模型说起。

线程可以视为进程中的控制流。一个进程至少会包含一个线程,因为其中至少会有一个控制流持续运行。因而,一个进程的第一个线程会随着这个进程的启动而创建,这个线程称为该进程的主线程。当然,一个进程也可以包含多个线程。这些线程都是由当前进程中已存在的线程创建出来的,创建的方法就是调用系统调用,更确切地说是调用
pthread create函数。拥有多个线程的进程可以并发执行多个任务,并且即使某个或某些任务被阻塞,也不会影响其他任务正常执行,这可以大大改善程序的响应时间和吞吐量。另一方面,线程不可能独立于进程存在。它的生命周期不可能逾越其所属进程的生命周期。

线程的实现模型主要有3个,分别是:用户级线程模型、内核级线程模型和两级线程模型。它们之间最大的差异就在于线程与内核调度实体( Kernel Scheduling Entity,简称KSE)之间的对应关系上。顾名思义,内核调度实体就是可以被内核的调度器调度的对象。在很多文献和书中,它也称为内核级线程,是操作系统内核的最小调度单元。

1.1 内核级线程模型
用户线程与KSE是1对1关系(1:1)。大部分编程语言的线程库(如linux的pthread,Java的java.lang.Thread,C++11的std::thread等等)都是对操作系统的线程(内核级线程)的一层封装,创建出来的每个线程与一个不同的KSE静态关联,因此其调度完全由OS调度器来做。这种方式实现简单,直接借助OS提供的线程能力,并且不同用户线程之间一般也不会相互影响。但其创建,销毁以及多个线程之间的上下文切换等操作都是直接由OS层面亲自来做,在需要使用大量线程的场景下对OS的性能影响会很大。
在这里插入图片描述
每个线程由内核调度器独立的调度,所以如果一个线程阻塞则不影响其他的线程。

优点:在多核处理器的硬件的支持下,内核空间线程模型支持了真正的并行,当一个线程被阻塞后,允许另一个线程继续执行,所以并发能力较强。

缺点:每创建一个用户级线程都需要创建一个内核级线程与其对应,这样创建线程的开销比较大,会影响到应用程序的性能。

1.2 用户级线程模型
用户线程与KSE是多对1关系(M:1),这种线程的创建,销毁以及多个线程之间的协调等操作都是由用户自己实现的线程库来负责,对OS内核透明,一个进程中所有创建的线程都与同一个KSE在运行时动态关联。现在有许多语言实现的 协程 基本上都属于这种方式。这种实现方式相比内核级线程可以做的很轻量级,对系统资源的消耗会小很多,因此可以创建的数量与上下文切换所花费的代价也会小得多。但该模型有个致命的缺点,如果我们在某个用户线程上调用阻塞式系统调用(如用阻塞方式read网络IO),那么一旦KSE因阻塞被内核调度出CPU的话,剩下的所有对应的用户线程全都会变为阻塞状态(整个进程挂起)。
所以这些语言的协程库会把自己一些阻塞的操作重新封装为完全的非阻塞形式,然后在以前要阻塞的点上,主动让出自己,并通过某种方式通知或唤醒其他待执行的用户线程在该KSE上运行,从而避免了内核调度器由于KSE阻塞而做上下文切换,这样整个进程也不会被阻塞了。
在这里插入图片描述
优点: 这种模型的好处是线程上下文切换都发生在用户空间,避免的模态切换(mode switch),从而对于性能有积极的影响。

缺点:所有的线程基于一个内核调度实体即内核线程,这意味着只有一个处理器可以被利用,在多处理器环境下这是不能够被接受的,本质上,用户线程只解决了并发问题,但是没有解决并行问题。如果线程因为 I/O 操作陷入了内核态,内核态线程阻塞等待 I/O 数据,则所有的线程都将会被阻塞,用户空间也可以使用非阻塞而 I/O,但是不能避免性能及复杂度问题。

1.3 两级线程模型
用户线程与KSE是多对多关系(M:N),这种实现综合了前两种模型的优点,为一个进程中创建多个KSE,并且线程可以与不同的KSE在运行时进行动态关联,当某个KSE由于其上工作的线程的阻塞操作被内核调度出CPU时,当前与其关联的其余用户线程可以重新与其他KSE建立关联关系。当然这种动态关联机制的实现很复杂,也需要用户自己去实现,这算是它的一个缺点吧。Go语言中的并发就是使用的这种实现方式,Go为了实现该模型自己实现了一个运行时调度器来负责Go中的"线程"与KSE的动态关联。此模型有时也被称为 混合型线程模型,即用户调度器实现用户线程到KSE的“调度”,内核调度器实现KSE到CPU上的调度。
在这里插入图片描述
二、Go并发调度: G-P-M模型
在操作系统提供的内核线程之上,Go搭建了一个特有的两级线程模型。goroutine机制实现了M : N的线程模型,goroutine机制是协程(coroutine)的一种实现,golang内置的调度器,可以让多核CPU中每个CPU执行一个协程。

2.1 调度器是如何工作的
有了上面的认识,我们可以开始真正的介绍Go的并发机制了,先用一段代码展示一下在Go语言中新建一个“线程”(Go语言中称为Goroutine)的样子:

// 用go关键字加上一个函数(这里用了匿名函数)
// 调用就做到了在一个新的“线程”并发执行任务
go func() {
    
     
    // do something in one new goroutine
}()
new java.lang.Thread(() -> {
    
     
    // do something in one new thread
}).start();

new java.lang.Thread(() -> {
// do something in one new thread
}).start();

Processor的数量是在启动时被设置为环境变量GOMAXPROCS的值,或者通过运行时调用函数GOMAXPROCS()进行设置。Processor数量固定意味着任意时刻只有GOMAXPROCS个线程在运行go代码。

我们分别用三角形,矩形和圆形表示Machine Processor和Goroutine。
在这里插入图片描述

在单核处理器的场景下,所有goroutine运行在同一个M系统线程中,每一个M系统线程维护一个Processor,任何时刻,一个Processor中只有一个goroutine,其他goroutine在runqueue中等待。一个goroutine运行完自己的时间片后,让出上下文,回到runqueue中。 多核处理器的场景下,为了运行goroutines,每个M系统线程会持有一个Processor。
在这里插入图片描述
在正常情况下,scheduler会按照上面的流程进行调度,但是线程会发生阻塞等情况,看一下goroutine对线程阻塞等的处理。

2.2 线程阻塞
当正在运行的goroutine阻塞的时候,例如进行系统调用,会再创建一个系统线程(M1),当前的M线程放弃了它的Processor,P转到新的线程中去运行。
在这里插入图片描述
2.3 runqueue执行完成
当其中一个Processor的runqueue为空,没有goroutine可以调度。它会从另外一个上下文偷取一半的goroutine。
在这里插入图片描述

其图中的G,P和M都是Go语言运行时系统(其中包括内存分配器,并发调度器,垃圾收集器等组件,可以想象为Java中的JVM)抽象出来概念和数据结构对象:
G:Goroutine的简称,上面用go关键字加函数调用的代码就是创建了一个G对象,是对一个要并发执行的任务的封装,也可以称作用户态线程。属于用户级资源,对OS透明,具备轻量级,可以大量创建,上下文切换成本低等特点。
M:Machine的简称,在linux平台上是用clone系统调用创建的,其与用linux pthread库创建出来的线程本质上是一样的,都是利用系统调用创建出来的OS线程实体。M的作用就是执行G中包装的并发任务。Go运行时系统中的调度器的主要职责就是将G公平合理的安排到多个M上去执行。其属于OS资源,可创建的数量上也受限了OS,通常情况下G的数量都多于活跃的M的。
P:Processor的简称,逻辑处理器,主要作用是管理G对象(每个P都有一个G队列),并为G在M上的运行提供本地化资源。

从两级线程模型来看,似乎并不需要P的参与,有G和M就可以了,那为什么要加入P这个东东呢?
其实Go语言运行时系统早期(Go1.0)的实现中并没有P的概念,Go中的调度器直接将G分配到合适的M上运行。但这样带来了很多问题,例如,不同的G在不同的M上并发运行时可能都需向系统申请资源(如堆内存),由于资源是全局的,将会由于资源竞争造成很多系统性能损耗,为了解决类似的问题,后面的Go(Go1.1)运行时系统加入了P,让P去管理G对象,M要想运行G必须先与一个P绑定,然后才能运行该P管理的G。这样带来的好处是,我们可以在P对象中预先申请一些系统资源(本地资源),G需要的时候先向自己的本地P申请(无需锁保护),如果不够用或没有再向全局申请,而且从全局拿的时候会多拿一部分,以供后面高效的使用。就像现在我们去政府办事情一样,先去本地政府看能搞定不,如果搞不定再去中央,从而提供办事效率。
而且由于P解耦了G和M对象,这样即使M由于被其上正在运行的G阻塞住,其余与该M关联的G也可以随着P一起迁移到别的活跃的M上继续运行,从而让G总能及时找到M并运行自己,从而提高系统的并发能力。
Go运行时系统通过构造G-P-M对象模型实现了一套用户态的并发调度系统,可以自己管理和调度自己的并发任务,所以可以说Go语言原生支持并发。自己实现的调度器负责将并发任务分配到不同的内核线程上运行,然后内核调度器接管内核线程在CPU上的执行与调度。

可以看到Go的并发用起来非常简单,用了一个语法糖将内部复杂的实现结结实实的包装了起来。其内部可以用下面这张图来概述:
在这里插入图片描述
写在最后,Go运行时完整的调度系统是很复杂,很难用一篇文章描述的清楚,这里只能从宏观上介绍一下,让大家有个整体的认识。

// Goroutine1
func task1() {
    
    
    go task2()
    go task3()
}

假如我们有一个G(Goroutine1)已经通过P被安排到了一个M上正在执行,在Goroutine1执行的过程中我们又创建两个G,这两个G会被马上放入与Goroutine1相同的P的本地G任务队列中,排队等待与该P绑定的M的执行,这是最基本的结构,很好理解。 关键问题是:
a.如何在一个多核心系统上尽量合理分配G到多个M上运行,充分利用多核,提高并发能力呢?
如果我们在一个Goroutine中通过go关键字创建了大量G,这些G虽然暂时会被放在同一个队列, 但如果这时还有空闲P(系统内P的数量默认等于系统cpu核心数),Go运行时系统始终能保证至少有一个(通常也只有一个)活跃的M与空闲P绑定去各种G队列去寻找可运行的G任务,该种M称为自旋的M。一般寻找顺序为:自己绑定的P的队列,全局队列,然后其他P队列。如果自己P队列找到就拿出来开始运行,否则去全局队列看看,由于全局队列需要锁保护,如果里面有很多任务,会转移一批到本地P队列中,避免每次都去竞争锁。如果全局队列还是没有,就要开始玩狠的了,直接从其他P队列偷任务了(偷一半任务回来)。这样就保证了在还有可运行的G任务的情况下,总有与CPU核心数相等的M+P组合 在执行G任务或在执行G的路上(寻找G任务)。
b. 如果某个M在执行G的过程中被G中的系统调用阻塞了,怎么办?
在这种情况下,这个M将会被内核调度器调度出CPU并处于阻塞状态,与该M关联的其他G就没有办法继续执行了,但Go运行时系统的一个监控线程(sysmon线程)能探测到这样的M,并把与该M绑定的P剥离,寻找其他空闲或新建M接管该P,然后继续运行其中的G,大致过程如下图所示。然后等到该M从阻塞状态恢复,需要重新找一个空闲P来继续执行原来的G,如果这时系统正好没有空闲的P,就把原来的G放到全局队列当中,等待其他M+P组合发掘并执行。

c. 如果某一个G在M运行时间过长,有没有办法做抢占式调度,让该M上的其他G获得一定的运行时间,以保证调度系统的公平性?
我们知道linux的内核调度器主要是基于时间片和优先级做调度的。对于相同优先级的线程,内核调度器会尽量保证每个线程都能获得一定的执行时间。为了防止有些线程"饿死"的情况,内核调度器会发起抢占式调度将长期运行的线程中断并让出CPU资源,让其他线程获得执行机会。当然在Go的运行时调度器中也有类似的抢占机制,但并不能保证抢占能成功,因为Go运行时系统并没有内核调度器的中断能力,它只能通过向运行时间过长的G中设置抢占flag的方法温柔的让运行的G自己主动让出M的执行权。
说到这里就不得不提一下Goroutine在运行过程中可以动态扩展自己线程栈的能力,可以从初始的2KB大小扩展到最大1G(64bit系统上),因此在每次调用函数之前需要先计算该函数调用需要的栈空间大小,然后按需扩展(超过最大值将导致运行时异常)。Go抢占式调度的机制就是利用在判断要不要扩栈的时候顺便查看以下自己的抢占flag,决定是否继续执行,还是让出自己。
运行时系统的监控线程会计时并设置抢占flag到运行时间过长的G,然后G在有函数调用的时候会检查该抢占flag,如果已设置就将自己放入全局队列,这样该M上关联的其他G就有机会执行了。但如果正在执行的G是个很耗时的操作且没有任何函数调用(如只是for循环中的计算操作),即使抢占flag已经被设置,该G还是将一直霸占着当前M直到执行完自己的任务。

runtime包

尽管 Go 编译器产生的是本地可执行代码,这些代码仍旧运行在 Go 的 runtime(这部分的代码可以在 runtime 包中找到)当中。这个 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收(第 10.8 节)、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。

一、常用函数
runtime 调度器是个非常有用的东西,关于 runtime 包几个方法:

NumCPU:返回当前系统的 CPU 核数量

GOMAXPROCS:设置最大的可同时使用的 CPU 核数

通过runtime.GOMAXPROCS函数,应用程序何以在运行期间设置运行时系统中得P最大数量。但这会引起“Stop the World”。所以,应在应用程序最早的调用。并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。

无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。

go1.8后,默认让程序运行在多个核上,可以不用设置了
go1.8前,还是要设置一下,可以更高效的利益cpu

Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行

这个函数的作用是让当前 goroutine 让出 CPU,当一个 goroutine 发生阻塞,Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞。

Goexit:退出当前 goroutine(但是defer语句会照常执行)

NumGoroutine:返回正在执行和排队的任务总数

runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。

注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。

GOOS:目标操作系统

runtime.GC:会让运行时系统进行一次强制性的垃圾收集

强制的垃圾回收:不管怎样,都要进行的垃圾回收。
非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
GOROOT :获取goroot目录

GOOS : 查看目标操作系统
很多时候,我们会根据平台的不同实现不同的操作,就而已用GOOS了:

。。。

二、示例代码:

获取goroot和os:

//获取goroot目录:
fmt.Println("GOROOT-->",runtime.GOROOT())

//获取操作系统
fmt.Println("os/platform-->",runtime.GOOS) // GOOS--> darwin,mac系统

获取CPU数量,和设置CPU数量:

func init(){
    
    
    //1.获取逻辑cpu的数量
    fmt.Println("逻辑CPU的核数:",runtime.NumCPU())
    //2.设置go程序执行的最大的:[1,256]
    n := runtime.GOMAXPROCS(runtime.NumCPU())
    fmt.Println(n)
}

Gosched():

func main() {
    
    
    go func() {
    
    
        for i := 0; i < 5; i++ {
    
    
            fmt.Println("goroutine。。。")
        }

    }()

    for i := 0; i < 4; i++ {
    
    
        //让出时间片,先让别的协议执行,它执行完,再回来执行此协程
        runtime.Gosched()
        fmt.Println("main。。")
    }
}

Goexit的使用(终止协程)

func main() {
    
    
    //创建新建的协程
    go func() {
    
    
        fmt.Println("goroutine开始。。。")

        //调用了别的函数
        fun()

        fmt.Println("goroutine结束。。")
    }() //别忘了()

    //睡一会儿,不让主协程结束
    time.Sleep(3*time.Second)
}

func fun() {
    
    
    defer fmt.Println("defer。。。")

    //return           //终止此函数
    runtime.Goexit() //终止所在的协程

    fmt.Println("fun函数。。。")
}

临界资源安全问题

一、临界资源
临界资源: 指并发环境中多个进程/线程/协程共享的资源。

但是在并发编程中对临界资源的处理不当, 往往会导致数据不一致的问题。

示例代码:

package main

import (
    "fmt"
    "time"
)

func main()  {
    
    
    a := 1
    go func() {
    
    
        a = 2
        fmt.Println("子goroutine。。",a)
    }()
    a = 3
    time.Sleep(1)
    fmt.Println("main goroutine。。",a)
}

二、临界资源安全问题
并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。

举个例子,我们通过并发来实现火车站售票这个程序。一共有100张票,4个售票口同时出售。

我们先来看一下示例代码:

package main

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

//全局变量
var ticket = 10 // 100张票

func main() {
    
    
    /*
    4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
     */
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100

    time.Sleep(5*time.Second)
}

func saleTickets(name string) {
    
    
    rand.Seed(time.Now().UnixNano())
    //for i:=1;i<=100;i++{
    
    
    //  fmt.Println(name,"售出:",i)
    //}
    for {
    
     //ticket=1
        if ticket > 0 {
    
     //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket)  // 1 , 0, -1 , -2
            ticket--   //0 , -1 ,-2 , -3
        } else {
    
    
            fmt.Println(name,"售罄,没有票了。。")
            break
        }
    }
}

我们为了更好的观察临界资源问题,每个goroutine先睡眠一个随机数,然后再售票,我们发现程序的运行结果,还可以卖出编号为负数的票。
分析:

我们的卖票逻辑是先判断票数的编号是否为负数,如果大于0,然后我们就进行卖票,只不过在卖票钱先睡眠,然后再卖,假如说此时已经卖票到只剩最后1张了,某一个goroutine持有了CPU的时间片,那么它再片段是否有票的时候,条件是成立的,所以它可以卖票编号为1的最后一张票。但是因为它在卖之前,先睡眠了,那么其他的goroutine就会持有CPU的时间片,而此时这张票还没有被卖出,那么第二个goroutine再判断是否有票的时候,条件也是成立的,那么它可以卖出这张票,然而它也进入了睡眠。。其他的第三个第四个goroutine都是这样的逻辑,当某个goroutine醒来的时候,不会再判断是否有票,而是直接售出,这样就卖出最后一张票了,然而其他的goroutine醒来的时候,就会陆续卖出了第0张,-1张,-2张。

这就是临界资源的不安全问题。某一个goroutine在访问某个数据资源的时候,按照数值,已经判断好了条件,然后又被其他的goroutine抢占了资源,并修改了数值,等这个goroutine再继续访问这个数据的时候,数值已经不对了。

三、临界资源安全问题的解决
要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕,解锁后,其他的goroutine才能来访问。

我们可以借助于sync包下的锁操作。

示例代码:

package main

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

//全局变量
var ticket = 10 // 100张票

var wg sync.WaitGroup
var matex sync.Mutex // 创建锁头

func main() {
    
    
    /*
    4个goroutine,模拟4个售票口,4个子程序操作同一个共享数据。
     */
    wg.Add(4)
    go saleTickets("售票口1") // g1,100
    go saleTickets("售票口2") // g2,100
    go saleTickets("售票口3") //g3,100
    go saleTickets("售票口4") //g4,100
    wg.Wait()              // main要等待。。。

    //time.Sleep(5*time.Second)
}

func saleTickets(name string) {
    
    
    rand.Seed(time.Now().UnixNano())
    defer wg.Done()
    //for i:=1;i<=100;i++{
    
    
    //  fmt.Println(name,"售出:",i)
    //}
    for {
    
     //ticket=1
        matex.Lock()
        if ticket > 0 {
    
     //g1,g3,g2,g4
            //睡眠
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            // g1 ,g3, g2,g4
            fmt.Println(name, "售出:", ticket) // 1 , 0, -1 , -2
            ticket--                         //0 , -1 ,-2 , -3
        } else {
    
    
            matex.Unlock() //解锁
            fmt.Println(name, "售罄,没有票了。。")
            break
        }
        matex.Unlock() //解锁
    }
}

四、写在最后
在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。

在Go语言中并不鼓励用锁保护共享状态的方式在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channel将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。

sync包-waitgroup

sync是synchronization同步这个词的缩写,所以也会叫做同步包。这里提供了基本同步的操作,比如互斥锁等等。这里除了Once和WaitGroup类型之外,大多数类型都是供低级库例程使用的。更高级别的同步最好通过channel通道和communication通信来完成

一、WaitGroup
WaitGroup,同步等待组。

在类型上,它是一个结构体。一个WaitGroup的用途是等待一个goroutine的集合执行完成。主goroutine调用了Add()方法来设置要等待的goroutine的数量。然后,每个goroutine都会执行并且执行完成后调用Done()这个方法。与此同时,可以使用Wait()方法来阻塞,直到所有的goroutine都执行完成。
二、Add()方法:
Add这个方法,用来设置到WaitGroup的计数器的值。我们可以理解为每个waitgroup中都有一个计数器
用来表示这个同步等待组中要执行的goroutin的数量。

如果计数器的数值变为0,那么就表示等待时被阻塞的goroutine都被释放,如果计数器的数值为负数,那么就会引发恐慌,程序就报错了。
三、Done()方法
Done()方法,就是当WaitGroup同步等待组中的某个goroutine执行完毕后,设置这个WaitGroup的counter数值减1。

其实Done()的底层代码就是调用了Add()方法:

// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
wg.Add(-1)
}COPY
四、Wait()方法
Wait()方法,表示让当前的goroutine等待,进入阻塞状态。一直到WaitGroup的计数器为零。才能解除阻塞,
这个goroutine才能继续执行。
五、示例代码:
我们创建并启动两个goroutine,来打印数字和字母,并在main goroutine中,将这两个子goroutine加入到一个WaitGroup中,同时让main goroutine进入Wait(),让两个子goroutine先执行。当每个子goroutine执行完毕后,调用Done()方法,设置WaitGroup的counter减1。当两条子goroutine都执行完毕后,WaitGroup中的counter的数值为零,解除main goroutine的阻塞。

示例代码:

package main

import (
    "fmt"
    "sync"
)
var wg sync.WaitGroup // 创建同步等待组对象
func main()  {
    
    
    /*
    WaitGroup:同步等待组
        可以使用Add(),设置等待组中要 执行的子goroutine的数量,

        在main 函数中,使用wait(),让主程序处于等待状态。直到等待组中子程序执行完毕。解除阻塞

        子gorotuine对应的函数中。wg.Done(),用于让等待组中的子程序的数量减1
     */
    //设置等待组中,要执行的goroutine的数量
    wg.Add(2)
    go fun1()
    go fun2()
    fmt.Println("main进入阻塞状态。。。等待wg中的子goroutine结束。。")
    wg.Wait() //表示main goroutine进入等待,意味着阻塞
    fmt.Println("main,解除阻塞。。")

}
func fun1()  {
    
    
    for i:=1;i<=10;i++{
    
    
        fmt.Println("fun1.。。i:",i)
    }
    wg.Done() //给wg等待中的执行的goroutine数量减1.同Add(-1)
}
func fun2()  {
    
    
    defer wg.Done()
    for j:=1;j<=10;j++{
    
    
        fmt.Println("\tfun2..j,",j)
    }
}

GOROOT=/usr/local/go #gosetup
GOPATH=/Users/ruby/go #gosetup
/usr/local/go/bin/go build -i -o /private/var/folders/kt/nlhsnpgn6lgd_q16f8j83sbh0000gn/T/___go_build_demo05_waitgroup_go /Users/ruby/go/src/l_goroutine/demo05_waitgroup.go #gosetup
/private/var/folders/kt/nlhsnpgn6lgd_q16f8j83sbh0000gn/T/___go_build_demo05_waitgroup_go #gosetup
fun1.。。i: 1
fun1.。。i: 2
fun1.。。i: 3
fun1.。。i: 4
fun1.。。i: 5
fun1.。。i: 6
fun1.。。i: 7
fun1.。。i: 8
fun1.。。i: 9
fun1.。。i: 10
main进入阻塞状态。。。等待wg中的子goroutine结束。。
    fun2..j, 1
    fun2..j, 2
    fun2..j, 3
    fun2..j, 4
    fun2..j, 5
    fun2..j, 6
    fun2..j, 7
    fun2..j, 8
    fun2..j, 9
    fun2..j, 10
main,解除阻塞。。

Process finished with exit code 0

接续篇

附加:包的函数的文档

https://golang.google.cn/pkg

猜你喜欢

转载自blog.csdn.net/weixin_45843419/article/details/124243479
go