Fork me on GitHub

随笔分类 - golang

golang笔记---函数

[TOC] # 介绍 函数是基本的代码块,除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。函数参数、返回值以及它们的类型被统称为函数签名。 函数被调用的基本格式如下: ``` pack1.Function(arg1, arg2, …, argn) ``` Function 是 pack1 包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参。函数被调用的时候,**这些实参将被复制**(简单而言)然后传递给被调用函数。 函数可以将其他函数调用作为它的参数,只要这个被调用函数的返回值个数、返回值类型和返回值的顺序与调用函数所需求的实参是一致的,例如: 假设 f1 需要 3 个参数 `f1(a, b, c int)`,同时 f2 返回 3 个参数 `f2(a, b int) (int, int, int)`,就可以这样调用 `f1:f1(f2(a, b))`。 ## 函数参数和返回值 函数能够接收参数供自己使用,也可以返回零个或多个值(我们通常把返回多个值称为返回一组值)。相比与 C、C++、Java 和 C#,多值返回是 Go 的一大特性,为我们判断一个函数是否正常执行提供了方便。 ## 按值传递和按引用传递 **Go 默认使用按值传递来传递参数,也就是传递参数的副本**。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量,比如 Function(arg1)。 **如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符号,比如 &variable)传递给函数,这就是按引用传递**,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们可以通过这个指针的值来修改这个值所指向的地址上的值。(译者注:指针也是变量类型,有自己的地址和值,通常指针的值指向一个变量的地址。所以,按引用传递也是按值传递。) 几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。 **在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)**。 有些函数只是完成一个任务,并没有返回值。我们仅仅是利用了这种函数的副作用,就像输出文本到终端,发送一个邮件或者是记录一个错误等。但是绝大部分的函数还是带有返回值的。 ## 命名的返回值 getX2AndX3 与 getX2AndX3_2 两个函数演示了如何使用**非命名返回值**与**命名返回值**的特性。 当需要返回多个非命名返回值时,需要使用 () 把它们括起来,比如 (int, int) 。 命名返回值作为结果形参(result parameters)**被初始化为相应类型的零值**,当需要返回的时候,我们**只需要一条简单的不带参数的return语句**。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来 ``` package main import "fmt" var num int = 10 var numx2, numx3 int func main() { numx2, numx3 = getX2AndX3(num) PrintValues() numx2, numx3 = getX2AndX3_2(num) PrintValues() } func getX2AndX3(input int) (int, int) { return 2 * input, 3 * input } func getX2AndX3_2(input int) (x2 int, x3 int) { x2 = 2 * input x3 = 3 * input // return x2, x3 return } func PrintValues() { fmt.Printf("num = %d, 2x num = %d, 3x num = %d\n", num, numx2, numx3) } ``` 即使函数使用了命名返回值,你依旧可以无视它而返回明确的值。 尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。 ## 空白符 空白符用来匹配一些不需要的值,然后丢弃掉 ThreeValues 是拥有三个返回值的不需要任何参数的函数,在下面的例子中,我们将第一个与第三个返回值赋给了 i1 与 f1。第二个返回值赋给了空白符 _,然后自动丢弃掉。 示例 6.4 blank_identifier.go ``` package main import "fmt" func main() { var i1 int var f1 float32 i1, _, f1 = ThreeValues() fmt.Printf("The int: %d, the float: %f \n", i1, f1) } func ThreeValues() (int, int, float32) { return 5, 6, 7.5 } ``` 输出结果: ``` The int: 5, the float: 7.500000 ``` ## 改变外部变量 传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力,所以被修改的变量不再需要使用 return 返回。如下的例子,reply 是一个指向 int 变量的指针,通过这个指针,我们在函数内修改了这个 int 变量的数值。 示例 6.6 side_effect.go ``` package main import ( "fmt" ) // this function changes reply: func Multiply(a, b int, reply *int) { *reply = a * b } func main() { n := 0 reply := &n Multiply(10, 5, reply) fmt.Println("Multiply:", *reply) // Multiply: 50 } ``` # 传递变长参数 如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数,注意只能用在最后一个参数。 ``` func myFunc(a, b, arg ...int) {} ``` 这个函数接受一个类似某个类型的 slice 的参数(详见第 7 章),该参数可以通过第 5.4.4 节中提到的 for 循环结构迭代。 示例函数和调用: ``` func Greeting(prefix string, who ...string) Greeting("hello:", "Joe", "Anna", "Eileen") ``` 在 Greeting 函数中,变量 who 的值为 []string{"Joe", "Anna", "Eileen"}。 如果参数被存储在一个 slice 类型的变量 slice 中,则可以通过 `slice...` 的形式来传递参数,调用变参函数。 示例 6.7 varnumpar.go ``` package main import "fmt" func main() { x := min(1, 3, 2, 0) fmt.Printf("The minimum is: %d\n", x) slice := []int{7,9,3,5,1} x = min(slice...) fmt.Printf("The minimum in the slice is: %d", x) } func min(s ...int) int { if len(s)==0 { return 0 } min := s[0] for _, v := range s { if v < min { min = v } } return min } ``` 输出: ``` The minimum is: 0 The minimum in the slice is: 1 ``` # defer 和追踪 关键字 defer 允许我们推迟到函数返回之前(或任意位置执行 return 语句之后)一刻才执行某个语句或函数(为什么要在返回之后才执行这些语句?因为 return 语句同样可以包含一些操作,而不是单纯地返回某个值)。 关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。 示例 6.8 defer.go: ``` package main import "fmt" func main() { function1() } func function1() { fmt.Printf("In function1 at the top\n") defer function2() fmt.Printf("In function1 at the bottom!\n") } func function2() { fmt.Printf("Function2: Deferred until the end of the calling function!") } ``` 输出: ``` In Function1 at the top In Function1 at the bottom! Function2: Deferred until the end of the calling function! ``` 使用 defer 的语句同样可以接受参数,下面这个例子就会在执行 defer 语句时打印 0: ``` func a() { i := 0 defer fmt.Println(i) i++ return } ``` 当有多个 defer 行为被注册时,**它们会以逆序执行**(类似栈,即后进先出): ``` func f() { for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) } } ``` 上面的代码将会输出:`4 3 2 1 0`。 关键字 defer 允许我们进行一些函数执行完成后的收尾工作,例如: - 关闭文件流 ``` // open a file defer file.Close() ``` - 解锁一个加锁的资源 ``` mu.Lock() defer mu.Unlock() ``` - 打印最终报告 ``` printHeader() defer printFooter() ``` - 关闭数据库链接 ``` // open a database connection defer disconnectFromDB() ``` 合理使用 defer 语句能够使得代码更加简洁。 # 将函数作为参数 函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。下面是一个将函数作为参数的简单例子(function_parameter.go): ``` package main import ( "fmt" ) func main() { callback(1, Add) } func Add(a, b int) { fmt.Printf("The sum of %d and %d is: %d\n", a, b, a+b) } func callback(y int, f func(int, int)) { f(y, 2) // this becomes Add(1, 2) } ``` 输出: ``` The sum of 1 and 2 is: 3 ``` # 闭包 当我们不希望给函数起名字的时候,可以使用匿名函数,例如:`func(x, y int) int { return x + y }`。 这样的一个函数**不能够独立存在**(编译器会返回错误:non-declaration statement outside function body),但**可以被赋值于某个变量**,即保存函数的地址到变量中:`fplus := func(x, y int) int { return x + y }`,然后**通过变量名对函数进行调用**:`fplus(3,4)`。 当然,您也可以**直接对匿名函数进行调用**:func(x, y int) int { return x + y } (3, 4)。 下面是一个计算从 1 到 1 百万整数的总和的匿名函数: ``` func() { sum := 0 for i := 1; i <= 1e6; i++ { sum += i } }() ``` 表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,**最后的一对括号表示对该匿名函数的调用**。 下面的例子展示了如何将匿名函数赋值给变量并对其进行调用(function_literal.go): ``` package main import "fmt" func main() { f() } func f() { for i := 0; i < 4; i++ { g := func(i int) { fmt.Printf("%d ", i) } //此例子中只是为了演示匿名函数可分配不同的内存地址,在现实开发中,不应该把该部分信息放置到循环中。 g(i) fmt.Printf(" - g is of type %T and has value %v\n", g, g) } } ``` 输出: ``` 0 - g is of type func(int) and has value 0x681a80 1 - g is of type func(int) and has value 0x681b00 2 - g is of type func(int) and has value 0x681ac0 3 - g is of type func(int) and has value 0x681400 ``` 我们可以看到变量 g 代表的是 func(int),变量的值是一个内存地址。 所以我们实际上拥有的是一个函数值:匿名函数可以被赋值给变量并作为值使用。 # 应用闭包:将函数作为返回值 6.9 应用闭包:将函数作为返回值 在程序 function_return.go 中我们将会看到函数 Add2 和 Adder 均会返回签名为 func(b int) int 的函数: ``` func Add2() (func(b int) int) func Adder(a int) (func(b int) int) ``` 我们也可以将 Adder 返回的函数存到变量中(function_return.go)。 ``` package main import "fmt" func main() { // make an Add2 function, give it a name p2, and call it: p2 := Add2() //p2(3)进行调用 fmt.Printf("Call Add2 for 3 gives: %v\n", p2(3)) // make a special Adder function, a gets value 2: TwoAdder := Adder(2) fmt.Printf("The result is: %v\n", TwoAdder(3)) } func Add2() func(b int) int { return func(b int) int { return b + 2 } } func Adder(a int) func(b int) int { return func(b int) int { return a + b } } ``` 输出: ``` Call Add2 for 3 gives: 5 The result is: 5 ``` 下例为一个略微不同的实现(function_closure.go): ``` package main import "fmt" func main() { var f = Adder() fmt.Print(f(1), " - ") fmt.Print(f(20), " - ") fmt.Print(f(300)) } func Adder() func(int) int { var x int return func(delta int) int { x += delta return x } } ``` 函数 Adder() 现在被赋值到变量 f 中(类型为 func(int) int)。 输出: ``` 1 - 21 - 321 ``` 三次调用函数 f 的过程中函数 Adder() 中变量 delta 的值分别为:`1、20 和 300`。 我们可以看到,在多次调用中,变量 x 的值是被保留的,即 `0 + 1 = 1`,然后 `1 + 20 = 21`,最后 `21 + 300 = 321`:闭包函数保存并积累其中的变量的值,不管外部函数退出与否,它都能够继续操作外部函数中的局部变量。 **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记--环境配置

[TOC] # 1.go的安装 Go有多种安装方式,你可以选择自己喜欢的。这里我们介绍三种最常见的安装方式: - Go源码安装:这是一种标准的软件安装方式。对于经常使用Unix类系统的用户,尤其对于开发者来说,从源码安装可以自己定制。 - Go标准包安装:Go提供了方便的安装包,支持Windows、Linux、Mac等系统。这种方式适合快速安装,可根据自己的系统位数下载好相应的安装包,一路next就可以轻松安装了。推荐这种方式 - 第三方工具安装:目前有很多方便的第三方软件包工具,例如Ubuntu的apt-get和wget、Mac的homebrew等。这种安装方式适合那些熟悉相应系统的用户。 这里介绍标准包安装 下载地址 ``` https://golang.org/dl/ ``` 下载到相应位置并解压 ``` cd /usr/local/src wget https://dl.google.com/go/go1.11.linux-amd64.tar.gz tar -xzvf go1.11.linux-amd64.tar.gz ``` 添加环境变量 ``` vim /etc/profile ``` ``` # go的安装位置 export GOROOT=/usr/local/src/go # go的工作目录地址 export GOPATH=/data/gopath # 方便运行go的bin和工作目录编译后的bin export PATH=$PATH:$GOROOT/bin:$GOPATH/bin ``` 应用环境变量 ``` source /etc/profile ``` 查看信息,如果显示则安装成功 ``` $ go version go version go1.11 linux/amd64 ``` ``` $ go env GOARCH="amd64" GOBIN="" GOCACHE="/root/.cache/go-build" GOEXE="" GOFLAGS="" GOHOSTARCH="amd64" GOHOSTOS="linux" GOOS="linux" GOPATH="/data/gopath" GOPROXY="" GORACE="" GOROOT="/usr/local/src/go" GOTMPDIR="" GOTOOLDIR="/usr/local/src/go/pkg/tool/linux_amd64" GCCGO="gccgo" CC="gcc" CXX="g++" CGO_ENABLED="1" GOMOD="" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config" GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build693193239=/tmp/go-build" ``` 创建hello.go文件 ``` touch hello.go vim hello.go ``` 内容 ``` package main import "fmt" func main() { fmt.Println("Hello, World!") } ``` 运行 ``` $ go run hello.go Hello, World! ``` # 2.gopath和工作空间 ## gopath的介绍 go 命令依赖一个重要的环境变量:$GOPATH,上面安装的时候我们有设置过这个路径,并添加到环境变量中。从go 1.8开始,GOPATH环境变量现在有一个默认值,如果它没有被设置。 它在Unix上默认为$HOME/go,在Windows上默认为%USERPROFILE%/go。 这个目录地址不是go的安装目录,而是你自己的工作目录,自己定义一个适合自己的位置。 GOPATH允许多个目录,当有多个目录时,请注意分隔符,多个目录的时候Windows是分号,Linux系统是冒号,当有多个GOPATH时,默认会将go get的内容放在第一个目录下。 以上 $GOPATH 目录约定有三个子目录: - src 存放源代码(比如:.go .c .h .s等) - pkg 编译后生成的文件(比如:.a) - bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用${GOPATH//://bin:}/bin添加所有的bin目录) ## 合理规划工作目录 GOPATH下的src目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,例如: `$GOPATH/src/mymath` 表示mymath这个**应用包或者可执行应用**,这个根据package是main还是其他来决定,**main的话就是可执行应用,其他的话就是应用包** 所以当新建应用或者一个代码包时都是在src目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在src下面新建了目录`$GOPATH/src/github.com/astaxie/beedb` 那么这个包路径就是`github.com/astaxie/beedb`,包名称是最后一个目录`beedb` ## 编写并编译应用 ▶ 编写一个应用包 ``` cd $GOPATH/src mkdir mymath ``` 新建文件sqrt.go,内容如下 ``` // $GOPATH/src/mymath/sqrt.go源码如下: package mymath func Sqrt(x float64) float64 { z := 0.0 for i := 0; i < 1000; i++ { z -= (z*z - x) / (2 * x) } return z } ``` 这样我的应用包目录和代码已经新建完毕,注意:一般建议package的名称和目录名保持一致 ▶ 编译应用 我们上面已经编辑了应用包,如何编译上面的应用包呢?有两种方式 1、进入对应的应用包目录,然后执行 `go install` 2、在任意目录,然后执行 `go install mymath` 执行完毕后,我们进入如下目录 ``` cd /data/gopath/pkg/linux_amd64 // cd $GOPATH/pkg/${GOOS}_${GOARCH} ``` 可以看到如下文件 ``` mymath.a ``` 这个.a文件是应用包,那么我们如何进行调用呢? 下面我们在创建一个可执行应用,在里面调用上面常见的mymath.a应用包 新建应用包mathapp ``` cd $GOPATH/src mkdir mathapp cd mathapp vim main.go ``` 内容: ``` //$GOPATH/src/mathapp/main.go源码: package main import ( "mymath" "fmt" ) func main() { fmt.Printf("Hello, world. Sqrt(2) = %v\n", mymath.Sqrt(2)) } ``` 如何编译一个可执行程序呢,方法是: 进入该目录下,然后运行`go build`, 会在该目录下生成一个mathapp的可执行文件 ``` # root @ VM_33_19_centos in /data/gopath/src/mathapp $ ./mathapp Hello, world. Sqrt(2) = 1.414213562373095 ``` 每次要指定相应目录在运行文件不是很麻烦,如何方便的安装该应用呢? 进入该目录执行`go install`,那么在`$GOPATH/bin/`下增加了一个可执行文件mathapp 我们在安装的时候已经把`$GOPATH/bin`加到我们的环境变量`PATH`里面,这样可以在命令行输入如下命令就可以执行 ``` mathapp ``` 也是输出如下内容 ``` Hello, world. Sqrt(2) = 1.414213562373095 ``` ## 获取远程包 go语言有一个获取远程包的工具就是`go get`,目前go get支持多数开源社区(例如:github、googlecode、bitbucket、Launchpad) ``` go get github.com/astaxie/beedb // go get -u 参数可以自动更新包,而且当go get的时候会自动获取该包依赖的其他第三方包 ``` 通过这个命令可以获取相应的源码,对应的开源平台采用不同的源码控制工具,例如github采用git、googlecode采用hg,所以要想获取这些源码,必须先安装相应的源码控制工具 通过上面获取的代码在我们本地的源码相应的代码结构如下 ``` $GOPATH src |--github.com |-astaxie |-beedb pkg |--相应平台 |-github.com |--astaxie |beedb.a ``` go get本质上可以理解为首先第一步是通过源码工具clone代码到src下面,然后执行go install 在代码中如何使用远程包,很简单的就是和使用本地包一样,只要在开头import相应的路径就可以 ``` import "github.com/astaxie/beedb" ``` ## 程序的整体结构 通过上面建立的我本地的mygo的目录结构如下所示 ``` bin/ mathapp pkg/ 平台名/ 如:darwin_amd64、linux_amd64 mymath.a github.com/ astaxie/ beedb.a src/ mathapp main.go mymath/ sqrt.go github.com/ astaxie/ beedb/ beedb.go util.go ``` 从上面的结构我们可以很清晰的看到,bin目录下面存的是编译之后可执行的文件,pkg下面存放的是应用包,src下面保存的是应用源代码 # 3.go 命令 go语言自带一套完整的命令操作工具 ``` # root @ VM_33_19_centos in /data/gopath/src [13:36:20] $ go 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 download and install packages and dependencies install compile and install packages and dependencies list list packages or modules mod module 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: 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 packages package lists and patterns testflag testing flags testfunc testing functions Use "go help <topic>" for more information about that topic. ``` ## go build 这个命令主要用于编译代码。在包的编译过程中,若有必要,会同时编译与之相关联的包。 - 如果是普通包,就像我们在1.2节中编写的mymath包那样,当你执行go build之后,它不会产生任何文件。如果你需要在$GOPATH/pkg下生成相应的文件,那就得执行go install。 - 如果是main包,当你执行go build之后,它就会在当前目录下生成一个可执行文件。如果你需要在$GOPATH/bin下生成相应的文件,需要执行go install,或者使用go build -o 路径/a.exe。 - 如果某个项目文件夹下有多个文件,而你只想编译某个文件,就可在go build之后加上文件名,例如go build a.go;go build命令默认会编译当前目录下的所有go文件。 - 你也可以指定编译输出的文件名。例如1.2节中的mathapp应用,我们可以指定go build -o astaxie.exe,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。 (注:实际上,package名在Go语言规范中指代码中“package”后使用的名称,此名称可以与文件夹名不同。默认生成的可执行文件名是文件夹名。) - go build会忽略目录下以“_”或“.”开头的go文件。 - 如果你的源代码针对不同的操作系统需要不同的处理,那么你可以根据不同的操作系统后缀来命名文件。例如有一个读取数组的程序,它对于不同的操作系统可能有如下几个源文件: array_linux.go array_darwin.go array_windows.go array_freebsd.go ## go install 这个命令在内部实际上分成了两步操作:第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin。 参数支持go build的编译参数。大家只要记住一个参数-v就好了,这个随时随地的可以查看底层的执行信息。 ## go get 这个命令是用来动态获取远程代码包的,目前支持的有BitBucket、GitHub、Google Code和Launchpad。这个命令在内部实际上分成了两步操作:第一步是下载源码包,第二步是执行go install。下载源码包的go工具会自动根据不同的域名调用不同的源码工具,对应关系如下: ``` BitBucket (Mercurial Git) GitHub (Git) Google Code Project Hosting (Git, Mercurial, Subversion) Launchpad (Bazaar) ``` 所以为了go get 能正常工作,你必须确保安装了合适的源码管理工具,并同时把这些命令加入你的PATH中。其实go get支持自定义域名的功能,具体参见go help remote。 参数介绍: ``` -d 只下载不安装 -f 只有在你包含了-u参数的时候才有效,不让-u去验证import中的每一个都已经获取了,这对于本地fork的包特别有用 -fix 在获取源码之后先运行fix,然后再去做其他的事情 -t 同时也下载需要为运行测试所需要的包 -u 强制使用网络去更新包和它的依赖包 -v 显示执行的命令 ``` ## go clean 这个命令是用来移除当前源码包和关联源码包里面编译生成的文件。这些文件包括 ``` _obj/ 旧的object目录,由Makefiles遗留 _test/ 旧的test目录,由Makefiles遗留 _testmain.go 旧的gotest文件,由Makefiles遗留 test.out 旧的test记录,由Makefiles遗留 build.out 旧的test记录,由Makefiles遗留 *.[568ao] object文件,由Makefiles遗留 DIR(.exe) 由go build产生 DIR.test(.exe) 由go test -c产生 MAINFILE(.exe) 由go build MAINFILE.go产生 *.so 由 SWIG 产生 ``` 参数介绍 ``` -i 清除关联的安装的包和可运行文件,也就是通过go install安装的文件 -n 把需要执行的清除命令打印出来,但是不执行,这样就可以很容易的知道底层是如何运行的 -r 循环的清除在import中引入的包 -x 打印出来执行的详细命令,其实就是-n打印的执行版本 ``` ## go fmt go工具集中提供了一个go fmt命令 它可以帮你格式化你写好的代码文件,使你写代码的时候不需要关心格式,你只需要在写完之后执行`go fmt <文件名>.go`,你的代码就被修改成了标准格式,但是我平常很少用到这个命令,因为开发工具里面一般都带了保存时候自动格式化功 使用go fmt命令,其实是调用了gofmt,而且需要参数-w,否则格式化结果不会写入文件。`gofmt -w -l src`,可以格式化整个项目。 所以go fmt是gofmt的上层一个包装的命令,我们想要更多的个性化的格式化可以参考 gofmt gofmt的参数介绍 ``` -l 显示那些需要格式化的文件 -w 把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。 -r 添加形如“a[b:len(a)] -> a[b:]”的重写规则,方便我们做批量替换 -s 简化文件中的代码 -d 显示格式化前后的diff而不是写入文件,默认是false -e 打印所有的语法错误到标准输出。如果不使用此标记,则只会打印不同行的前10个错误。 -cpuprofile 支持调试模式,写入相应的cpufile到指定的文件 ``` ## 更多命令 ``` go version 查看go当前的版本 go env 查看当前go的环境变量 go list 列出当前全部安装的package go run 编译并运行Go程序 ``` 用户可以使用go help 命令获取更详细的帮助信息 # 4.go get 被墙解决方案 由于墙的存在,我们使用go get的时候经常没反应还没有报错,如果是新手这会浪费我们大量的时间 解决方案 国内主机:cow + 国外可用代理地址:我这里使用了shadowsocks 关于如何搭建代理服务器,请自行谷歌,这里提下国内云服务器(linux)如何配置 关于cow的详细使用,可以参考:https://github.com/cyfdecyf/cow/blob/master/doc/init.d/cow 安装: ``` cd /usr/local/src curl -L git.io/cow | bash ``` 打开配置文件 ``` vim ~/.cow/rc ``` 增加如下配置 ``` listen = http://127.0.0.1:7777 # 代理的地址,配置文件中有详细参数介绍,根据自己的情况配置 proxy = ss://aes-256-cfb:[email protected]:13000 ``` 环境变量中增加如下配置 ``` vim /etc/profile ``` ``` export http_proxy=http://127.0.0.1:7777 export https_proxy=http://127.0.0.1:7777 ``` ``` source /etc/profile ``` 启动cow,进入`/usr/local/src` ``` ./cow & ``` 查看是否配置成功 ``` $ echo $https_proxy http://127.0.0.1:7777 # root @ VM_33_19_centos in /usr/local/src $ echo $http_proxy http://127.0.0.1:7777 # root @ VM_33_19_centos in /usr/local/src $ ps -ef | grep cow root 2644 1 0 14:56 pts/1 00:00:00 sudo -u root -H -- /usr/local/src/cow root 2646 2644 0 14:56 pts/1 00:00:00 /usr/local/src/cow root 3462 1469 0 15:04 pts/0 00:00:00 grep --color=auto cow ``` 这里提下,cow还提供了一个启动脚本,参考地址:https://github.com/cyfdecyf/cow/blob/master/doc/init.d/cow 简单提下用法: 下载脚本放到 /etc/init.d/下 注意修改下脚本下面的配置,根据自己的情况修改 ``` vim /etc/init.d/cow ``` ``` BIN=/usr/local/bin/cow USER=usr GROUP=grp ``` 使用方法: ``` /etc/init.d/cow start /etc/init.d/cow stop /etc/init.d/cow restart /etc/init.d/cow status ``` 配置完毕 可以畅快的go get 了 # 5.window下开发工具 可以使用goland,官方网站:https://www.jetbrains.com/go/ ,直接下载 有条件请支持正版,但是还是给个激活学习通道,http://idea.lanyus.com/ ,需要**注意**的是 ``` 使用前请将“0.0.0.0 account.jetbrains.com”添加到hosts文件中 ``` 方便之处在于高亮和快速跳转等,可以方便的编译,可以提高我们的工作效率。 # 6.go的包管理工具dep 官方github地址:https://github.com/golang/dep 官方文档:https://golang.github.io/dep/docs/introduction.html ## 安装 ### linux下的安装,直接使用脚本 ``` $ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh ``` ``` # root @ WENGINE in /var/www/gopath/src/webvpn [10:01:17] $ curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 5110 100 5110 0 0 2366 0 0:00:02 0:00:02 --:--:-- 12342 ARCH = amd64 OS = linux Will install into /var/www/gopath/bin Fetching https://github.com/golang/dep/releases/latest.. Release Tag = v0.5.0 Fetching https://github.com/golang/dep/releases/tag/v0.5.0.. Fetching https://github.com/golang/dep/releases/download/v0.5.0/dep-linux-amd64.. Setting executable permissions. Moving executable to /var/www/gopath/bin/dep ``` ### windows下安装 方式1: 使用go get 获取执行程序,执行程序会被放到$gopath/bin目录下,所以记得把该目录添加到环境变量即可 ``` go get -u github.com/golang/dep/cmd/dep ``` 方式2: 直接下载程序 - 下载release版本。打开页面 https://github.com/golang/dep/releases ,下载最新的dep-windows-amd64.exe - 将dep-windows-amd64.exe放入GOPATH/bin下,修改名称为dep ## 使用 1、初始化 - 切换到工程目录下 `cd %GOPATH%/src/github.com/xxx/dep-demo` - 初始化 `dep init` 2、添加到vendor中 ``` dep ensure -add github.com/go-macaron/captcha ``` ``` D:\code\gopath\src\webvpn>dep ensure -add github.com/go-macaron/captcha Fetching sources... grouped write of manifest, lock and vendor: scratch directory D:\code\gopath\src\webvpn\.vendor-new already exists, please remove it D:\code\gopath\src\webvpn>dep ensure -add github.com/go-macaron/captcha Fetching sources... "github.com/go-macaron/captcha" is not imported by your project, and has been temporarily added to Gopkg.lock and vendor/. If you run "dep ensure" again before actually importing it, it will disappear from Gopkg.lock and vendor/. ``` 参考资料 https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记--基本结构和基本数据类型

[TOC] # 1.文件名 Go 的源文件以 .go 为后缀名存储在计算机中,这些文件名均由小写字母组成,如 scanner.go 。如果文件名由多个部分组成,则使用下划线 _ 对它们进行分隔,如 scanner_test.go 。文件名不包含空格或其他特殊字符。 # 2.Go 程序的基本结构和要素 ## 2.1包的概念、导入与可见性 ### 相关概念 每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成,因此文件名和包名一般来说都是不相同的。 每个 Go 应用程序都包含一个名为 main 的包,你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个**可独立执行的程序**,**每个 Go 应用程序含一个名为 main 的包**。 一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你**可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main 来指明这些文件都属于 main 包**。如果你打算编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而**不是可执行程序**。另外要注意的是,所有的**包名**都应该使用**小写字母**。 在 Go 的安装文件里包含了一些可以直接使用的包,即标准库。在 Linux 下,标准库在 Go 根目录下的子目录 pkg\linux_amd64 中(如果是安装的是 32 位,则在 linux_386 目录中)。一般情况下,标准包会存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目录下。 属于同一个包的源文件必须全部被一起编译,一个包即是编译时的一个单元,因此根据惯例,**每个目录都只包含一个包**。 如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。 ### 如何导入包 ``` import "fmt" import "os" ``` 或: ``` import "fmt"; import "os" ``` 或 ``` import ( "fmt" "os" ) ``` 它甚至还可以更短的形式,但使用 gofmt 后将会被强制换行: ``` import ("fmt"; "os") ``` - 当你导入多个包时,**最好按照字母顺序排列包名**,这样做更加清晰易读。 - 如果包名不是以 . 或 / 开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找; - 如果包名以 ./ 开头,则 Go 会在相对目录中查找; - 如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。 ### 可见性规则 当标识符(包括常量、变量、类型、函数名、结构字段等等)**以一个大写字母开头**,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);**标识符如果以小写字母开头,则对包外是不可见的**,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。 因此,在导入一个外部包后,能够且只能够访问该包中导出的对象。 假设在包 pack1 中我们有一个变量或函数叫做 Thing(以 T 开头,所以它能够被导出),那么在当前包中导入 pack1 包,Thing 就可以像面向对象语言那样使用点标记来调用:pack1.Thing(pack1 在这里是不可以省略的)。 因此包也可以作为命名空间使用,帮助避免命名冲突(名称冲突):两个包中的同名变量的区别在于他们的包名,例如 pack1.Thing 和 pack2.Thing。 你可以通过使用包的别名来解决包名之间的名称冲突,或者说根据你的个人喜好对包名进行重新设置,如:`import fm "fmt"`。 ## 2.2函数 这是定义一个函数最简单的格式: ``` func functionName() ``` **main 函数是每一个可执行程序所必须包含的**,一般来说都是在启动后**第一个执行的函数**(**如果有 init() 函数则会先执行该函数**)。如果你的 main 包的源代码没有包含 main 函数,则会引发构建错误 undefined: main.main。**main 函数既没有参数,也没有返回类型**(与 C 家族中的其它语言恰好相反)。如果你不小心为 main 函数添加了参数或者返回类型,将会引发构建错误。 在程序开始执行并完成初始化后,第一个调用(程序的入口点)的函数是 main.main(),该函数一旦返回就表示程序已成功执行并立即退出。 左大括号 `{` 必须与方法的声明放在同一行,右大括号 `}` 需要被放在紧接着函数体的下一行。 如果你的函数非常简短,你也可以将它们放在同一行: ``` func Sum(a, b int) int { return a + b } ``` 因此符合规范的函数一般写成如下的形式: ``` func functionName(parameter_list) (return_value_list) { … } ``` 其中: - 参数(parameter_list) 的形式为 (param1 type1, param2 type2, …) - 返回值 (return_value_list) 的形式为 (ret1 type1, ret2 type2, …) 只有当某个函数需要**被外部包调用的时候才使用大写字母开头**,并遵循 Pascal 命名法; 否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。 ## 2.3类型 使用 var 声明的变量的值会**自动初始化为该类型的零值**。 类型可以是基本类型, - 如:int、float、bool、string; - 如:struct、array、slice、map、channel; - 只描述类型的行为的,如:interface。 **一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来**,如: ``` func FunctionName (a typea, b typeb) (t1 type1, t2 type2) ``` 示例: 函数 Atoi : ``` func Atoi(s string) (i int, err error) ``` 返回的形式: ``` return var1, var2 ``` 使用 type 关键字可以定义你自己的类型,你可能想要定义一个结构体,但是也可以定义一个已经存在的类型的别名,如: ``` type IZ int ``` 这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。 然后我们可以使用下面的方式声明变量: ``` var a IZ = 5 ``` 这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能。 **如果你有多个类型需要定义,可以使用因式分解关键字的方式**,例如: ``` type ( IZ int FZ float64 STR string ) ``` ## 2.4Go 程序的一般结构 总体思路如下: - 在完成包的 import 之后,开始对常量、变量和类型的定义或声明。 - 如果存在 init 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。 - 如果当前包是 main 包,则定义 main 函数。 - 然后定义其余的函数,首先是类型的方法,接着是按照 main 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。 示例 gotemplate.go ``` package main import ( "fmt" ) const c = "C" var v int = 5 type T struct{} func init() { // initialization of package } func main() { var a int Func1() // ... fmt.Println(a) } func (t T) Method1() { //... } func Func1() { // exported function Func1 //... } ``` Go 程序的执行(程序启动)顺序如下: - 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程: - 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。 - 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init 函数的话,则调用该函数。 - 在完成这一切之后,main 也执行同样的过程,最后调用 main 函数开始执行程序。 # 3.常量 常量使用关键字 const 定义,用于**存储不会改变的数据**。 存储在常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。 常量的定义格式:`const identifier [type] = value`,例如: ``` const Pi = 3.14159 ``` 在 Go 语言中,你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。 - 显式类型定义: const b string = "abc" - 隐式类型定义: const b = "abc" 常量的值必须是能够在编译时就能够确定的;你可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。 ``` 正确的做法:const c1 = 2/3 错误的做法:const c2 = getNumber() // 引发构建错误: getNumber() used as value ``` 因为在编译期间自定义函数均属于未知,因此无法用于常量的赋值,但内置函数可以使用,如:len()。 数字型的常量是没有大小和符号的,并且可以使用任何精度而不会导致溢出: ``` const Ln2= 0.693147180559945309417232121458\ 176568075500134360255254120680009 const Log2E= 1/Ln2 // this is a precise reciprocal const Billion = 1e9 // float constant const hardEight = (1 << 100) >> 97 ``` 根据上面的例子我们可以看到,**反斜杠 \ 可以在常量表达式中作为多行的连接符使用**。 常量也允许使用并行赋值的形式: ``` const beef, two, c = "eat", 2, "veg" const Monday, Tuesday, Wednesday, Thursday, Friday, Saturday = 1, 2, 3, 4, 5, 6 const ( Monday, Tuesday, Wednesday = 1, 2, 3 Thursday, Friday, Saturday = 4, 5, 6 ) ``` 常量还可以用作枚举: ``` const ( Unknown = 0 Female = 1 Male = 2 ) ``` 现在,数字 0、1 和 2 分别代表未知性别、女性和男性。 # 4.变量 ## 简介 声明变量的一般形式是使用 var 关键字:`var identifier type`。 在 Go 中,则可以很轻松地将它们都声明为指针类型: ``` var a, b *int ``` 其次,这种语法能够按照从左至右的顺序阅读,使得代码更加容易理解。 示例: ``` var a int var b bool var str string ``` 你也可以改写成这种形式: ``` var ( a int b bool str string ) ``` **这种因式分解关键字的写法一般用于声明全局变量**。 当一个变量被声明之后,系统自动赋予它该类型的零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。记住,所有的内存在 Go 中都是经过初始化的。 变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShips 和 startDate。 但如果你的全局变量希望能够被外部包所使用,则需要将首个单词的首字母也大写 一个变量(常量、类型或函数)在程序中都有一定的作用范围,称之为作用域。如果一个变量在函数体外声明,则被认为是全局变量,可以在整个包甚至外部包(被导出后)使用,不管你声明在哪个源文件里或在哪个源文件里调用该变量。 在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。 赋值给变量使用运算符等号 =。 示例: ``` a = 15 b = false ``` 声明与赋值(初始化)语句也可以组合起来。 示例: ``` var identifier [type] = value var a int = 15 var i = 5 var b bool = false var str string = "Go says hello to the world!" ``` Go 编译器的智商已经高到可以根据变量的值来自动推断其类型,这有点像 Ruby 和 Python 这类动态语言,只不过它们是在运行时进行推断,而 Go 是在编译时就已经完成推断过程。因此,你还可以使用下面的这些形式来声明及初始化变量: ``` var a = 15 var b = false var str = "Go says hello to the world!" ``` 或: ``` var ( a = 15 b = false str = "Go says hello to the world!" numShips = 50 city string ) ``` 变量的类型也可以在运行时实现自动推断,例如: ``` var ( HOME = os.Getenv("HOME") USER = os.Getenv("USER") GOROOT = os.Getenv("GOROOT") ) ``` **这种写法主要用于声明包级别的全局变量,当你在函数体内声明局部变量时,应使用简短声明语法 :=**,例如: ``` a := 1 ``` ## 简短形式,使用 := 赋值操作符 ``` a := 50 b := false ``` a 和 b 的类型(int 和 bool)将由编译器自动推断。 这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。 **注意事项:** - 在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 - 给相同的变量赋予一个新的值是可以的,例如:a = 20 - 在定义变量 a 之前使用它,则会得到编译错误 undefined: a - 声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,单纯地给 a 赋值也是不够的,这个值必须被使用;但是全局变量是允许声明但不使用 其他的简短形式为: 同一类型的多个变量可以声明在同一行,如: ``` var a, b, c int ``` (这是将类型写在标识符后面的一个重要原因) 多变量可以在同一行进行赋值,如: ``` a, b, c = 5, 7, "abc" ``` 上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用: ``` a, b, c := 5, 7, "abc" ``` 这被称为 并行 或 同时 赋值。 如果你想要交换两个变量的值,则可以简单地使用 ``` a, b = b, a。 ``` 空白标识符 _ 也被用于抛弃值,如值 5 在:`_, b = 5, 7 `中被抛弃。 _ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。 ## 值类型和引用类型 ### 指针 程序在内存中存储它的值,每个内存块(或字)有一个地址,通常用十六进制数表示,如:0x6b0820 或 0xf84001d7f0。 ``` var i1 = 5 fmt.Printf("An integer: %d, it's location in memory: %p\n", i1, &i1) ``` 上面的代码片段可能输出 An integer: 5, its location in memory: 0x6b0820(这个值随着你每次运行程序而变化,指针的格式化标识符为 %p) **Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址**。这个地址可以存储在一个叫做指针的特殊数据类型中,在本例中这是一个指向 int 的指针,即 i1:此处使用 *int 表示。如果我们想调用指针 intP,我们可以这样声明它: ``` var intP *int ``` 然后使用 ``` intP = &i1 ``` 此时 intP 指向 i1。intP 存储了 i1 的内存地址;它指向了 i1 的位置,它引用了变量 i1。 ![](http://markdown.archerwong.cn/2019-01-02-03-55-55_clipboard.png) **你可以在指针类型前面加上 * 号(前缀)来获取指针所指向的内容**,这里的 * 号是一个类型更改器,如 *intP,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;另一种说法是指针转移。 当一个指针被定义后没有分配到任何变量时,它的值为 nil。 一个指针变量通常缩写为 ptr。 ### 值类型 **int、float、bool 和 string这些基本类型都属于值类型,另外,像数组和结构这些复合类型也是值类型** ▶ 使用这些类型的变量直接指向存在内存中的值: ![](http://markdown.archerwong.cn/2019-01-02-03-56-43_clipboard.png) ▶ 当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝,**修改j的时候并不会影响到i**。 ![](http://markdown.archerwong.cn/2019-01-02-03-57-15_clipboard.png) ### 引用类型 **指针属于引用类型,其它的引用类型还包括 slices,maps和 channel** 更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。 ▶ 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。 ![](http://markdown.archerwong.cn/2019-01-02-03-57-47_clipboard.png) 这个内存地址被称之为指针,这个指针实际上也被存在另外的某一个字中。 当使用赋值语句 r2 = r1 时,只有引用(地址)被复制;**如果 r2 的值被改变了,r1也会受到影响,所有引用都会指向被修改后的内容**。 ## init 函数 变量除了可以在全局声明中初始化,也可以在 init 函数中初始化。这是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。 每个源文件都只能包含一个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。 一个可能的用途是在开始执行程序之前对数据进行检验或修复,以保证程序状态的正确性。 **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记---接口(interface)

[TOC] # 1.接口介绍 ## 接口的定义 golang中接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。 定义接口: ``` type Namer interface { Method1(param_list) return_type Method2(param_list) return_type ... } ``` - 类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。 - 实现某个接口的类型(除了实现接口方法外)可以有其他的方法。 - 一个类型可以实现多个接口。 - 接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型) 举例 ``` package main import "fmt" type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } func main() { sq1 := new(Square) sq1.side = 5 var areaIntf Shaper areaIntf = sq1 fmt.Printf("The square has area: %f\n", areaIntf.Area()) } ``` 输出: ``` The square has area: 25.000000 ``` 上面的程序定义了一个结构体 `Square` 和一个接口 `Shaper`,接口有一个方法 `Area()`。 在 `main()` 方法中创建了一个 Square 的实例。在主程序外边定义了一个接收者类型是 `Square` 方法的 `Area()`,用来计算正方形的面积:结构体 `Square` 实现了接口 `Shaper` 。 所以可以将一个 `Square` 类型的变量赋值给一个接口类型的变量:`areaIntf = sq1` 。 现在接口变量包含一个指向 `Square` 变量的引用,通过它可以调用 `Square` 上的方法 `Area()`。当然也可以直接在 `Square` 的实例上调用此方法,但是在接口实例上调用此方法更令人兴奋,它使此方法更具有一般性。接口变量里包含了接收者实例的值和指向对应方法表的指针。 如果 `Square` 没有实现 `Area()` 方法,编译器将会给出清晰的错误信息;如果 Shaper 有另外一个方法 Perimeter(),但是Square 没有实现它,即使没有人在 Square 实例上调用这个方法,编译器也会给出上面同样的错误。 ## 展示多态 示例 interfaces_poly.go: ``` package main import "fmt" type Shaper interface { Area() float32 } type Square struct { side float32 } func (sq *Square) Area() float32 { return sq.side * sq.side } type Rectangle struct { length, width float32 } func (r Rectangle) Area() float32 { return r.length * r.width } func main() { r := Rectangle{5, 3} // Area() of Rectangle needs a value q := &Square{5} // Area() of Square needs a pointer // shapes := []Shaper{Shaper(r), Shaper(q)} // or shorter shapes := []Shaper{r, q} fmt.Println("Looping through shapes for area ...") for n, _ := range shapes { fmt.Println("Shape details: ", shapes[n]) fmt.Println("Area of this shape is: ", shapes[n].Area()) } } ``` 输出: ``` Looping through shapes for area ... Shape details: {5 3} Area of this shape is: 15 Shape details: &{5} Area of this shape is: 25 ``` 在调用 shapes[n].Area() 这个时,只知道 shapes[n] 是一个 Shaper 对象,最后它摇身一变成为了一个 Square 或 Rectangle 对象,并且表现出了相对应的行为。 # 2.接口嵌套 一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。 ``` type ReadWrite interface { Read(b Buffer) bool Write(b Buffer) bool } type Lock interface { Lock() Unlock() } type File interface { ReadWrite Lock Close() } ``` # 3.类型断言 一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值: ``` v := varI.(T) // unchecked type assertion ``` **varI 必须是一个接口变量**,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left) 。 类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言: ``` if v, ok := varI.(T); ok { // checked type assertion Process(v) return } // varI is not of type T ``` 如果转换合法,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有运行时错误发生。 应该总是使用上面的方式来进行类型断言。 多数情况下,我们可能只是想在 if 中测试一下 ok 的值,此时使用以下的方法会是最方便的: ``` if _, ok := varI.(T); ok { // ... } ``` # 4. 方法集和接口的应用 **作用于变量上的方法实际上是不区分变量到底是指针还是值的。当碰到接口类型值时,这会变得有点复杂** ``` package main import ( "fmt" ) //定义接口 appender type Appender interface { Append(int) } // 定义接口 lener type Lener interface { Len() int } //定义一个结构体和方法集 type List []int func (l List) Len() int { return len(l) } func (l *List) Append(val int) { *l = append(*l, val) } //函数 func CountInto(a Appender, start, end int) { for i := start; i <= end; i++ { a.Append(i) } } //函数 func LongEnough(l Lener) bool { return l.Len()*10 > 42 } func main() { // 值 var lst List // CountInto(lst, 1, 10) 会报错,conutinto需要一个appender,是个指针 if LongEnough(lst) { fmt.Printf("- lst is long enough\n") } // 指针 plst := new(List) CountInto(plst, 1, 10) if LongEnough(plst) { fmt.Printf("- plst is long enough\n") } } ``` - 在 lst 上调用 CountInto 时会导致一个编译器错误,因为 CountInto 需要一个 Appender,而它的方法 Append 只定义在指针上。 在 lst 上调用 LongEnough 是可以的,因为 Len 定义在值上。 - 在 plst 上调用 CountInto 是可以的,因为 CountInto 需要一个 Appender,并且它的方法 Append 定义在指针上。 在 plst 上调用 LongEnough 也是可以的,因为指针会被自动解引用。 **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记---结构(struct)和方法(method)

[TOC] # 1.前言 一个带属性的结构体试图表示一个现实世界中的实体。结构体是复合类型(composite types),当需要定义一个类型,它由一系列属性组成,每个属性都有自己的类型和值的时候,就应该使用结构体,它把数据聚集在一起。然后可以访问这些数据,就好像它是一个独立实体的一部分。结构体也是值类型,因此可以通过 new 函数来创建。 组成结构体类型的那些数据称为 字段(fields)。每个字段都有一个类型和一个名字;在一个结构体中,字段名字必须是唯一的。 结构体的概念在软件工程上旧的术语叫 ADT(抽象数据类型:Abstract Data Type),在一些老的编程语言中叫 记录(Record) 在golang中struct 类似于类的作用,我们知道类有属性和方法,这里结构体定义的时候,只是对类中的属性进行定义赋值,并没有对方法进行定义,但是,方法可以随时定义绑定到该类的对象上,更具灵活性,并且可利用嵌套组合来实现类似继承的功能避免代码重复。 体会下典型例子 ``` type Rect struct{ //定义矩形类 x,y float64 //类型只包含属性,并没有方法 width,height float64 } //为Rect类型绑定Area的方法, //*Rect为指针引用,可以修改传入参数的值,如果为func (r Rect) Area() float64{ ... }则不会 func (r *Rect) Area() float64{ return r.width*r.height //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法 //在Go语言中没有隐藏的this指针,即显示传递,形参即为this,这里形参为r。 } ``` # 2.结构体 ## 定义 一般: ``` type identifier struct { field1 type1 field2 type2 ... } ``` 简单的结构体可以简写。 ``` type T struct {a, b int} ``` **结构体的字段可以是任何类型**,甚至是结构体本身,也可以是函数或者接口。可以声明结构体类型的一个变量,然后像下面这样给它的字段赋值: ``` var s T s.a = 5 s.b = 8 ``` **数组可以看作是一种结构体类型,不过它使用下标而不是具名的字段**。 ## 声明和初始化 ### 声明和初始化实例 ``` package main import "fmt" type foo struct { name string age int } func main() { //声明并初始化 var foo1 foo foo1.name = "archer" foo1.age = 18 //声明并使用字面量 var foo2 foo foo2 = foo{"archer", 2} //简短写法 foo3 := foo{"archer", 3} // 混合字面量语法,&foo{"archer", 4}是一种简写,底层仍会调用new() foo4 := &foo{"archer", 4} fmt.Println("foo4:",foo4) //使用new(),表达式 new(Type) 和 &Type{} 是等价的 var foo5 *foo foo5 = new(foo) foo5.name = "archer" foo5.age = 5 fmt.Println("foo5:",foo5) // 声明和初始化在一行 var foo6 *foo = new(foo) foo6.name = "archer" foo6.age = 6 fmt.Println("foo6:",foo6) // 简短写法 foo7 := new(foo) foo7.name = "archer" foo7.age = 7 fmt.Println(foo1, foo2, foo3, foo4, foo5, foo6, foo7) } ``` ``` {archer 18} {archer 2} {archer 3} &{archer 4} &{archer 5} &{archer 6} &{archer 7} ``` - 可以使用点号符给字段赋值:`structname.fieldname = value`。 - 同样的,使用点号符可以获取结构体字段的值:`structname.fieldname`。 - 无论变量是一个结构体类型还是一个结构体类型指针,都可以使用同样的方式获取字段值或给字段赋值: ### 图解初始化 使用new初始化 ![](http://markdown.archerwong.cn/2019-01-02-03-58-34_clipboard.png) 使用结构体字面量初始化 ![](http://markdown.archerwong.cn/2019-01-02-03-58-49_clipboard.png) ## 使用工厂方法创造结构体实例 假设定义了如下的 File 结构体类型: ``` //如果想强制别的包必须使用工厂方法创建实例,根据可见性规则,那么File应该小写 type File struct { fd int // 文件描述符 name string // 文件名 } ``` 下面是这个结构体类型对应的工厂方法,它返回一个指向结构体实例的指针: ``` func NewFile(fd int, name string) *File { if fd < 0 { return nil } return &File{fd, name} } ``` 然后这样调用它: ``` f := NewFile(10, "./test.txt") ``` ## 带标签的结构体 结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。比如:在一个变量上调用 reflect.TypeOf() 可以获取变量的正确类型,如果变量是一个结构体类型,就可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。 示例 10.7 struct_tag.go: ``` package main import ( "fmt" "reflect" ) type TagType struct { // tags field1 bool "An important answer" field2 string "The name of the thing" field3 int "How much there are" } func main() { tt := TagType{true, "Barak Obama", 1} for i := 0; i < 3; i++ { refTag(tt, i) } } func refTag(tt TagType, ix int) { ttType := reflect.TypeOf(tt) ixField := ttType.Field(ix) fmt.Printf("%v\n", ixField.Tag) } ``` 输出: ``` An important answer The name of the thing How much there are ``` ## 匿名字段和内嵌结构体 ### 实例 结构体可以包含一个或多个 匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。 可以粗略地将这个和面向对象语言中的继承概念相比较,它可以用来模拟类似继承的行为。Go 语言中的继承是通过内嵌或组合来实现的。 示例 structs_anonymous_fields.go: ``` package main import "fmt" type innerS struct { in1 int in2 int } type outerS struct { b int c float32 int // anonymous field innerS //anonymous field } func main() { outer := new(outerS) outer.b = 6 outer.c = 7.5 outer.int = 60 outer.in1 = 5 outer.in2 = 10 fmt.Printf("outer.b is: %d\n", outer.b) fmt.Printf("outer.c is: %f\n", outer.c) fmt.Printf("outer.int is: %d\n", outer.int) fmt.Printf("outer.in1 is: %d\n", outer.in1) fmt.Printf("outer.in2 is: %d\n", outer.in2) // 使用结构体字面量 outer2 := outerS{6, 7.5, 60, innerS{5, 10}} fmt.Println("outer2 is:", outer2) } ``` 输出: ``` outer.b is: 6 outer.c is: 7.500000 outer.int is: 60 outer.in1 is: 5 outer.in2 is: 10 outer2 is:{6 7.5 60 {5 10}} ``` 通过类型 outer.int 的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:在一个结构体中对于每一种数据类型只能有一个匿名字段。 ### 命名冲突 当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢? - 外层名字会覆盖内层名字(但是两者的内存空间都保留),这提供了一种重载字段或方法的方式; - 如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误(不使用没关系)。没有办法来解决这种问题引起的二义性,必须由程序员自己修正。 例子: 现有结构体A 和 B ``` type A struct {a int} type B struct {a, b int} ``` 规则1:使用 d.b 是没问题的:它是 float32,而不是 B 的 b。如果想要内层的 b 可以通过 d.B.b 得到。 ``` type D struct {B; b float32} var d D ``` 规则 2:使用 c.a 是错误的,到底是 c.A.a 还是 c.B.a 呢?会导致编译器错误:ambiguous DOT reference c.a disambiguate with either c.A.a or c.B.a。 ``` type C struct {A; B} var c C ``` # 3.方法 ## 简介 在 Go 语言中,结构体就像是类的一种简化形式,一个类型加上它的方法等价于面向对象中的一个类。Go方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此**方法是一种特殊类型的函数**。 接收者类型可以是(几乎)任何类型,不仅仅是结构体类型:任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型。但是接收者不能是一个接口类型 最后接收者不能是一个指针类型,但是它可以是任何其他允许类型的指针。 类型 T(或 *T)上的所有方法的集合叫做类型 T(或 *T)的方法集。 因为方法是函数,所以同样的,**不允许方法重载**,即对于一个类型只能有一个给定名称的方法。但是不同的接收者类型,是可以使用相同名称的: ``` func (a *denseMatrix) Add(b Matrix) Matrix func (a *sparseMatrix) Add(b Matrix) Matrix ``` ## 定义方法 ``` func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... } ``` recv 就像是面向对象语言中的 this 或 self,但是 Go 中并没有这两个关键字。随个人喜好,你可以使用 this 或 self 作为 receiver 的名字。 示例 method .go: ``` package main import "fmt" type TwoInts struct { a int b int } func main() { two1 := new(TwoInts) two1.a = 12 two1.b = 10 fmt.Printf("The sum is: %d\n", two1.AddThem()) //输出22 } func (tn *TwoInts) AddThem() int { return tn.a + tn.b } ``` ## 指针或值作为接收者区别 ``` package main import ( "fmt" ) type B struct { thing int } //recv(b)是一个指向 receiver_type(B) 的指针 func (b *B) change(num int) { b.thing = num } //receiver_type是普通类型值 func (b B) write() string { return fmt.Sprint(b) } func main() { var b1 B // b1是值 b1.change(1) fmt.Println(b1.write()) b2 := new(B) // b2是指针 b2.change(2) fmt.Println(b2.write()) } ``` 结果 ``` {1} {2} ``` 如果改变change为值方法 ``` func (b B) change(num int) { b.thing = num } ``` 则上面的结果为 ``` {0} {0} ``` - b1和b2分别是值和指针,但是都可以调用change 和 write方法,**指针方法和值方法都可以在指针或非指针上被调用** - 因为change是一个指针方法,所以我们可以在方法内修改实例的属性thing,并且会改变原始实例的值。 - 指针方法和值方法的区别在于,指针方法可以修改recv的值,并且因为是指针传递,传递的代价要比值方法低很多。 ## 内嵌类型的方法和继承 当一个匿名类型被内嵌在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。 内嵌结构体上的方法可以直接在外层类型的实例上调用: ``` package main import ( "fmt" "math" ) type Point struct { x, y float64 } func (p *Point) Abs() float64 { return math.Sqrt(p.x*p.x + p.y*p.y) } type NamedPoint struct { Point name string } func main() { n := &NamedPoint{Point{3, 4}, "Pythagoras"} fmt.Println(n.Abs()) // 打印5 } ``` NamedPoint里面嵌套着Point,我们可以直接实例化NamePoint之后使用Abs方法。内嵌将一个已存在类型的字段和方法注入到了另一个类型里:匿名字段上的方法“晋升”成为了外层类型的方法。当然类型可以有只作用于本身实例而不作用于内嵌“父”类型上的方法,可以覆写方法(像字段一样):和内嵌类型方法具有同样名字的外层类型的方法会覆写内嵌类型对应的方法。 上面例子,添加如下方法 ``` func (n *NamedPoint) Abs() float64 { return n.Point.Abs() * 100. } ``` 现在 fmt.Println(n.Abs()) 会打印 500。 ## 多重继承 多重继承指的是类型获得多个父类型行为的能力,在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。 如下所示: ``` package main import ( "fmt" ) type Camera struct{} func (c *Camera) TakeAPicture() string { return "Click" } type Phone struct{} func (p *Phone) Call() string { return "Ring Ring" } type CameraPhone struct { Camera Phone } func main() { cp := new(CameraPhone) fmt.Println("Our new CameraPhone exhibits multiple behaviors...") fmt.Println("It exhibits behavior of a Camera: ", cp.TakeAPicture()) fmt.Println("It works like a Phone too: ", cp.Call()) } ``` 输出: ``` Our new CameraPhone exhibits multiple behaviors... It exhibits behavior of a Camera: Click It works like a Phone too: Ring Ring ``` **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记---控制结构

[TOC] # if-else 结构 ``` if condition { // do something } ``` ``` if condition { // do something } else { // do something } ``` ``` if condition1 { // do something } else if condition2 { // do something else } else { // catch-all or default } ``` 关键字 if 和 else 之后的左大括号 `{` 必须和关键字在同一行,如果你使用了 else-if 结构,则前段代码块的右大括号 `}` 必须和 else-if 关键字在同一行。这两条规则都是被编译器强制规定的。 实例:判断一个字符串是否为空: ``` if str == "" { ... } if len(str) == 0 {...} ``` if 可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号): ``` if initialization; condition { // do something } ``` 例如: ``` val := 10 if val > max { // do something } ``` 你也可以这样写: ``` if val := 10; val > max { // do something } ``` 但要注意的是,使用简短方式 := 声明的变量的作用域只存在于 if 结构中(在 if 结构的大括号之间,如果使用 if-else 结构则在 else 代码块中变量也会存在)。 如果变量在 if 结构之前就已经存在,那么在 if 结构中,该变量原来的值会被隐藏。最简单的解决方案就是不要在初始化语句中声明变量 示例 ifelse.go ``` package main import "fmt" func main() { var cond int if cond = 5; cond > 10 { fmt.Printf("cond is greater than 10\n") } else { fmt.Printf("cond is not greater than 10\n") } } ``` 输出: ``` cond is not greater than 10 ``` # switch 结构 ``` switch var1 { case val1: ... case val2: ... default: ... } ``` **变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值**。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。**前花括号 { 必须和 switch 关键字在同一行**。 您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:`case val1, val2, val3`。 每一个 case 分支都是唯一的,从上至下逐一测试,直到匹配为止。一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说您**不需要特别使用 break 语句来表示结束**。 因此,程序也不会自动地去执行下一个分支的代码。如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 `fallthrough` 关键字来达到目的。 因此: ``` switch i { case 0: // 空分支,只有当 i == 0 时才会进入分支 case 1: f() // 当 i == 0 时函数不会被调用 } ``` 并且: ``` switch i { case 0: fallthrough case 1: f() // 当 i == 0 时函数也会被调用 } ``` 可以使用 return 语句来提前结束代码块的执行。当您在 switch 语句块中使用 return 语句,并且您的函数是有返回值的,您还需要在 switch 之后添加相应的 return 语句以确保函数始终会返回。 # for 结构 ## 基于计数器的迭代 文件 for1.go 中演示了最简单的基于计数器的迭代,基本形式为: ``` for 初始化语句; 条件语句; 修饰语句 {} ``` 示例 5.6 for1.go: ``` package main import "fmt" func main() { for i := 0; i < 5; i++ { fmt.Printf("This is the %d iteration\n", i) } } ``` 输出: ``` This is the 0 iteration This is the 1 iteration This is the 2 iteration This is the 3 iteration This is the 4 iteration ``` 循环的头部由三部分组成,它们之间使用分号 ; 相隔,**但并不需要括号 () 将它们括起来**。例如:for (i = 0; i < 10; i++) { },这是无效的代码!同样的,左花括号 { 必须和 for 语句在同一行,计数器的生命周期在遇到右花括号 } 时便终止。 两个 for 循环嵌套起来: ``` for i:=0; i<5; i++ { for j:=0; j<10; j++ { println(j) } } ``` ## 基于条件判断的迭代 for 结构的第二种形式是没有头部的条件判断迭代(类似其它语言中的 while 循环),基本形式为:for 条件语句 {}。 ``` package main import "fmt" func main() { var i int = 5 for i >= 0 { i = i - 1 fmt.Printf("The variable i is now: %d\n", i) } } ``` 输出: ``` The variable i is now: 4 The variable i is now: 3 The variable i is now: 2 The variable i is now: 1 The variable i is now: 0 The variable i is now: -1 ``` ## 无限循环 条件语句是可以被省略的,如 `i:=0; ; i++` 或 `for { }` 或 `for ;; { }` `;;` 会在使用 gofmt 时被移除这些循环的本质就是无限循环。最后一个形式也可以被改写为 for true { },但一般情况下都会直接写 for { }。 如果 for 循环的头部没有条件语句,那么就会认为条件永远为 true,因此循环体内必须有相关的条件判断以确保会在某个时刻退出循环。 想要直接退出循环体,可以使用 break 语句或 return 语句直接返回。 但这两者之间有所区别,**break 只是退出当前的循环体,而 return 语句提前对函数进行返回,不会执行后续的代码**。 ## For-range结构 可以迭代任何一个集合,可以应用于数组和切片等 ``` for ix, value := range slice1 { ... } ``` 第一个返回值 ix 是数组或者切片的索引,第二个是在该索引位置的值;他们都是仅在 for 循环内部可见的**局部变量**。**value 只是 slice1 某个索引位置的值的一个拷贝**,不能用来修改 slice1 该索引位置的值。 示例 7.9 slices_forrange.go ``` package main import "fmt" func main() { var slice1 []int = make([]int, 4) slice1[0] = 1 slice1[1] = 2 slice1[2] = 3 slice1[3] = 4 for ix, value := range slice1 { fmt.Printf("Slice at %d is: %d\n", ix, value) } } ``` 结果: ``` Slice at 0 is: 1 Slice at 1 is: 2 Slice at 2 is: 3 Slice at 3 is: 4 ``` # beark 和 continue break 语句的作用结果是跳过整个代码块,执行后续的代码,**break 只会退出最内层的循环**: 示例 for4.go: ``` package main func main() { for i:=0; i<3; i++ { for j:=0; j<10; j++ { if j>5 { break } print(j) } print(" ") } } ``` 输出: ``` 012345 012345 012345 ``` 关键字 continue 忽略剩余的循环体而**直接进入下一次循环的过程**,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。 示例 for5.go: ``` package main func main() { for i := 0; i < 10; i++ { if i == 5 { continue } print(i) print(" ") } } ``` 输出: ``` 0 1 2 3 4 6 7 8 9 ``` 显然,5 被跳过了。 另外,**关键字 continue 只能被用于 for 循环中**。 # 标签与goto for、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(gofmt 会将后续代码自动移至下一行)。 标签的名称是大小写敏感的,为了提升可读性,**一般建议使用全部大写字母** ``` package main import "fmt" func main() { LABEL1: for i := 0; i <= 5; i++ { for j := 0; j <= 5; j++ { if j == 4 { continue LABEL1 } fmt.Printf("i is: %d, and j is: %d\n", i, j) } } } ``` 本例中,continue 语句指向 LABEL1,当执行到该语句的时候,就会跳转到 LABEL1 标签的位置。 您可以看到当 j==4 和 j==5 的时候,没有任何输出:标签的作用对象为外部循环,因此 i 会直接变成下一个循环的值,而此时 j 的值就被重设为 0,即它的初始值。如果将 continue 改为 break,则不会只退出内层循环,而是直接退出外层循环了。 另外,还可以使用 goto 语句和标签配合使用来模拟循环。 示例 5.14 goto.go: ``` package main func main() { i:=0 HERE: print(i) i++ if i==5 { return } goto HERE } ``` 上面的代码会输出 01234。 使用逆向的 goto 会很快导致意大利面条式的代码,所以不应当使用而选择更好的替代方案。 **特别注意: 使用标签和 goto 语句是不被鼓励的**:它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方案来实现相同的需求。 **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记---数组(array)和切片(slice)

[TOC] # 数组 ## 概念 数组是具有相同 **唯一类型** 的一组已编号且 **长度固定** 的数据项序列(这是一种同构的数据结构);数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。 **▶ 声明方式1:** 声明的格式是: ``` var identifier [len]type ``` 例如: ``` var arr1 [5]int ``` ![](http://markdown.archerwong.cn/2019-01-02-03-59-52_clipboard.png) 每个元素是一个整型值,当声明数组时所有的元素都会被自动初始化为默认值 0。 arr1 的长度是 5,索引范围从 0 到 len(arr1)-1。 第一个元素是 arr1[0],第三个元素是 arr1[2];总体来说索引 i 代表的元素是 arr1[i],最后一个元素是 arr1[len(arr1)-1]。 **对索引项为 i 的数组元素赋值可以这么操作:arr[i] = value,所以数组是可变的**。 **▶ 声明方式2: var arr1 = new([5]int) 和 var arr2 [5]int 的区别** 数组可以通过 new() 来创建: `var arr1 = new([5]int)` ,得到的是一个**指向数组的指针**。 那么这种方式和 `var arr2 [5]int` 的区别是什么呢? ``` arr1 的类型是 *[5]int,而 arr2的类型是 [5]int。 ``` 结果如下: ``` var arr1 [5]int arr2 := arr1 arr2[2] = 100 fmt.Println(arr1) fmt.Println(arr2) //以下是指向数组的指针,使用new关键字,同样也可以直接操作 var a1 = new ([5]int) a2 := a1 a2[2] = 100 fmt.Println(a1) fmt.Println(a2) ``` ``` [0 0 0 0 0] [0 0 100 0 0] &[0 0 100 0 0] &[0 0 100 0 0] ``` 通过上面的结果,可以看出,在赋值后修改arr2不会arr1的值,修改a2同时会修改a1的值。 函数中数组作为参数传入时,如 func1(arr2),会产生一次数组拷贝,func1 方法不会修改原始的数组 arr2。如果你想修改原数组,那么可以使用下面的方式: **方法1**: arr2 必须**通过&操作符以引用方式传过来**,例如 func1(&arr2),下面是一个例子 示例 pointer_array.go: ``` package main import "fmt" func f(a [3]int) { fmt.Println(a) } func fp(a *[3]int) { fmt.Println(a) } //该函数内使用引用传递,可以修改原数组 func main() { var ar [3]int f(ar) // passes a copy of ar fp(&ar) // passes a pointer to ar } ``` 输出结果: ``` [0 0 0] &[0 0 0] ``` **方法2**: 另一种方法就是**生成数组切片**并将其传递给函数 后面关于切片章节会讲到 ## 数组常量 如果数组值已经提前知道了,那么可以通过 **数组常量** 的方法来初始化数组,而不用依次使用 []= 方法(所有的组成元素都有相同的常量语法) 第一种变化: ``` var arrAge = [5]int{18, 20, 15, 22, 16} ``` 注意 [5]int 可以从左边起开始忽略:[10]int {1, 2, 3} :这是一个有 10 个元素的数组,除了前三个元素外其他元素都为 0。 第二种变化: ``` var arrLazy = [...]int{5, 6, 7, 8, 22} ``` **... 可同样可以忽略,从技术上说它们其实变化成了切片**。 ``` var arrLazy = []int{5, 6, 7, 8, 22} //实际上是创建了一个长度为 5 的数组并且创建了一个相关切片。 ``` 第三种变化:key: value syntax ``` var arrKeyValue = [5]string{3: "Chris", 4: "Ron"} ``` 只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串。 ## 多维数组 数组通常是一维的,但是可以用来组装成多维数组,例如:`[3][5]int,[2][2][2]float64`。 ``` a: = [2][3]int{ {1:1}, {2:2} } fmt.Println(a) ``` 结果 ``` [[0 1 0] [0 0 2]] ``` 内部数组总是长度相同的。Go 语言的多维数组是矩形式的(唯一的例外是切片的数组)。 示例 multidim_array.go ``` package main const ( WIDTH = 1920 HEIGHT = 1080 ) type pixel int var screen [WIDTH][HEIGHT]pixel func main() { for y := 0; y < HEIGHT; y++ { for x := 0; x < WIDTH; x++ { screen[x][y] = 0 } } } ``` ## 将数组传递给函数 把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象: - 传递数组的指针 - 使用数组的切片 **传递数组的指针在 Go 中并不常用,通常使用切片** # 切片 ## 概念 切片(slice)是对数组一个连续片段的引用(该数组我们称之为**相关数组**,通常是匿名的),所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内,可以理解为数学中的 `[)`。 给定项的切片索引可能比相关数组的相同元素的索引小。**和数组不同的是,切片的长度可以在运行时修改**,最小为 0 最大为相关数组的长度:切片是一个 **长度可变的数组**。 切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少:它等于切片的长度 + 数组除切片之外的长度。如果 s 是一个切片,cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s 来说该不等式永远成立:`0 <= len(s) <= cap(s)`。 多个切片如果表示同一个数组的片段,它们可以共享数据;因此**一个切片和相关数组的其他切片是共享存储的吗,其中一个值改变会影响全部**,相反,**不同的数组总是代表不同的存储**。 **优点** 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。 声明切片的格式是: ``` var identifier []type //(不需要说明长度,申明一个数组 var a [5]int)。 ``` 一个切片在未初始化之前默认为 nil,长度为 0。 切片的初始化格式是: ``` var slice1 []type = arr1[start:end]。 ``` 这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式) 让slice1等于完整的arr1数组,,可以用下面的方式 ``` var slice1 []type = arr1[0:len(arr1)] //表达式全范围 var slice1 []type = arr1[:] //上面的一种简写 slice1 = &arr1 //另一种表述方式 ``` 简写 ``` arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。 arr1[:3] 和 arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第三个)。 ``` 一个由数字 1、2、3 组成的切片可以这么生成: ``` s := [3]int{1,2,3}[:] //(注: 应先用s := [3]int{1, 2, 3}生成数组, 再使用s[:]转成切片) 甚至更简单的 s := []int{1,2,3}。 //创建了一个长度为 3 的数组并且创建了一个相关切片。 ``` `s2 := s[:]` 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组,也就是原始的数组。 切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。下图给出了一个长度为 2,容量为 4 的切片y。 - y[0] = 3 且 y[1] = 5。 - 切片 y[0:4] 由 元素 3,5,7 和 11 组成。 ![](http://markdown.archerwong.cn/2019-01-02-04-00-11_clipboard.png) **注意 绝对不要用指针指向 slice。切片本身已经是一个引用类型,所以它本身就是一个指针!!** ## 将切片传递给函数 如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个 切片引用并传递给该函数。 这里有一个计算数组元素和的方法: ``` func sum(a []int) int { s := 0 for i := 0; i < len(a); i++ { s += a[i] } return s } func main() { var arr = [5]int{0, 1, 2, 3, 4} sum(arr[:]) } ``` ## 使用make()创建一个分片 当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组: ``` var slice1 []type = make([]type, len) ``` 也可以简写为 ``` slice1 := make([]type, len) ``` 这里 len 是数组的长度并且也是 slice 的初始长度。所以定义 `s2 := make([]int, 10)`,那么 `cap(s2) == len(s2) == 10`。 如果你想创建一个 slice1,它不占用整个数组,而只是占用以 len 为个数个项,那么只要: ``` slice1 := make([]type, len, cap)。 ``` make 的使用方式是:`func make([]T, len, cap)`,其中 cap 是可选参数。 所以下面两种方法可以生成相同的切片: ``` make([]int, 50, 100) new([100]int)[0:50] ``` 下图描述了使用 make 方法生成的切片的内存结构: ![](http://markdown.archerwong.cn/2019-01-02-04-00-25_clipboard.png) ## new()和make()的区别 二者都在堆上分配内存,但是它们的行为不同,**适用于不同的类型**。 - new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为**T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针**,它适用于值类型如数组和结构体;它相当于 &T{}。 - make(T) **返回一个类型为 T 的初始值**,它只适用于3种内建的引用类型:切片、map 和 channel。 换言之,new 函数分配内存,make 函数初始化;下图给出了区别: ![](http://markdown.archerwong.cn/2019-01-02-04-00-41_clipboard.png) 实例分析: ``` var a = new ([5]int) lena := len(a) capa := cap(a) var b = make([]int, 5, 10) lenb := len(b) capb := cap(b) fmt.Println(a) fmt.Println(lena) fmt.Println(capa) fmt.Println(b) fmt.Println(lenb) fmt.Println(capb) ``` 结果 ``` &[0 0 0 0 0] 5 5 [0 0 0 0 0] 5 10 ``` ## 切片重组 我们已经知道切片创建的时候通常比相关数组小,例如: ``` slice1 := make([]type, start_length, capacity) ``` 其中 start_length 作为切片初始长度而 capacity 作为相关数组的长度。 这么做的好处是我们的切片在达到容量上限后可以扩容。**改变切片长度的过程称之为切片重组 reslicing**,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。 将切片扩展 1 位可以这么做: ``` sl = sl[0:len(sl)+1] ``` 切片可以反复扩展直到占据整个相关数组。 另一个例子: ``` var ar = [10]int{0,1,2,3,4,5,6,7,8,9} var a = ar[5:7] // reference to subarray {5,6} - len(a) is 2 and cap(a) is 5 ``` 将 a 重新分片: ``` a = a[0:4] // ref of subarray {5,6,7,8} - len(a) is now 4 but cap(a) is still 5 ``` ## 切片的复制和追加 如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。 `func append(s[]T, x ...T) []T` 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了,这时就要注意不会影响到其它切片了。append 方法总是返回成功,除非系统内存耗尽了。 ``` a := []int{1,2,3} b := append(a, 4, 5) // [1 2 3 4 5] ``` `func copy(dst, src []T) int copy` 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 src 和 dst 的长度最小值。如果 src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,在拷贝结束后执行 src = dst。 ``` package main import "fmt" func main() { sl := []int{1, 2, 3, 4, 5, 6} s2 := []int{7, 8, 9} copy(s1, s2) fmt.Println(s1) } ``` ``` [7, 8, 9, 4, 5, 6] ``` **参考资料** https://github.com/Unknwon/the-way-to-go_ZH_CN/blob/master/eBook/directory.md

golang笔记---字符串操作整理

[TOC] # 判断字符串 s 是否以 prefix 开头 ``` strings.HasPrefix(s, prefix string) bool ``` # 判断字符串 s 是否以 suffix 结尾 ``` strings.HasSuffix(s, suffix string) bool ``` # 判断字符串 s 是否包含 substr ``` strings.Contains(s, substr string) bool ``` # 判断子字符串或字符在父字符串中出现的位置(索引) ``` //Index 返回字符串 str 在字符串 s 中的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str strings.Index(s, str string) int //LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字// 符串 str strings.LastIndex(s, str string) int //如果 ch 是非 ASCII 编码的字符,建议使用以下函数来对字符进行定位 strings.IndexRune(s string, ch int) int ``` # 字符串替换 ``` //Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n < 0 则替换所有字符串 old 为字符串 new strings.Replace(str, old, new, n) string ``` # 统计字符串出现次数 ``` //Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数 strings.Count(s, str string) int ``` # 重复字符串 ``` //Repeat 用于重复 count 次字符串 s 并返回一个新的字符串 strings.Repeat(s, count int) string ``` # 修改字符串大小写 ``` //ToLower 将字符串中的 Unicode 字符全部转换为相应的小写字符 strings.ToLower(s) string //ToUpper 将字符串中的 Unicode 字符全部转换为相应的大写字符 strings.ToUpper(s) string ``` # 修剪字符串 ``` //来剔除字符串开头和结尾的空白符号 strings.TrimSpace(s) //如果你想要剔除指定字符,将开头和结尾的 cut 去除掉。该函数的第二个参数可以包含任何字符,如果你只想剔除开头或者结尾的字符串,则可以使用 TrimLeft 或者 TrimRight 来实现 strings.Trim(s, "cut") // 删除首部和尾部的 ! 和空格 fmt.Printf("%q\n", strings.Trim(" !!! Achtung! Achtung! !!! ", "! ")) // "Achtung! Achtung" fmt.Printf("%q\n", strings.TrimSpace(" \t\n a lone gopher \n\t\r\n")) // "a lone gopher" ``` # 分割字符串 ``` strings.Fields(s) //将会利用 1 个或多个空白符号来作为动态长度的分隔符将字符串分割成若干小块,并返回一个 slice,如果字符串只包含空白符号,则返回一个长度为 0 的 slice。 strings.Split(s, sep) //用于自定义分割符号来对指定字符串进行分割,同样返回 slice。因为这 2 个函数都会返回 slice,所以习惯使用 for-range 循环来对其进行处理 ``` # 拼接 slice 到字符串 ``` //Join 用于将元素类型为 string 的 slice 使用分割符号来拼接组成一个字符串 Strings.Join(sl []string, sep string) ``` 以上资料基本来自: https://my.oschina.net/ivandongqifan/blog/486354

golang笔记---golang发起get和post请求

[TOC] # 直接get: ``` func httpGet() { resp, err := http.Get("http://www.01happy.com/demo/accept.php?id=1") if err != nil { // handle error } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { // handle error } fmt.Println(string(body)) } ``` # 直接post: ``` func httpPost() { resp, err := http.Post("http://www.01happy.com/demo/accept.php", "application/x-www-form-urlencoded", strings.NewReader("name=cjb")) if err != nil { fmt.Println(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { // handle error } fmt.Println(string(body)) } ``` **注意只要是发送post请求时候请带上Content-Type参数("application/x-www-form-urlencoded")** # 复杂情况: ``` func httpDo() { client := &http.Client{} req, err := http.NewRequest("POST", "http://www.01happy.com/demo/accept.php", strings.NewReader("name=cjb")) if err != nil { // handle error } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Cookie", "name=anny") resp, err := client.Do(req) defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { // handle error } fmt.Println(string(body)) } ``` 生成request时候的参数配置 NewRequest函数有三个基本的参数,NewRequest(method, urlStr string, body io.Reader) - 第一个是请求的类型,GET, POST, PUT, etc.要设成大写的形式。 - 第二个参数是请求要访问的url, - 第三个参数是请求的body中的内容,需要是一个io.Reader的类型。 注意io.Reader的接口中是一个Read方法,实现了Read方法的类型应该都可以作为io.Reader来返回,`Read(p []byte) (n int, err error)`函数具体的功能就是读入len(p)长度的内容到p中,返回读入的长度以及错误信息。 通常是采用strings.NewReader函数,将一个string类型转化为io.Reader类型,或者bytes.NewBuffer函数,将[]byte类型转化为io.Reader类型。 此外还可以给request的header中添加一些额外的信息,比如下面例子中添加了请求的body的类型以及token的信息。 ``` reqest.Header.Set("Content-Type", "application/x-www-form-urlencoded") reqest.Header.Set("Authorization", "qwertyuiopasdfghjklzxcvbnm1234567890") ``` 还有比如模拟表单提交,可以把提交的类型设置为url.Values类型再进行Encode: ``` // use map as struct var clusterinfo = url.Values{} //var clusterinfo = map[string]string{} clusterinfo.Add("userName", user) clusterinfo.Add("password", pw) clusterinfo.Add("cloudName", clustername) clusterinfo.Add("masterIp", masterip) clusterinfo.Add("cacrt", string(caCrt)) data := clusterinfo.Encode() url := "https://10.10.105.124:8443/user/checkAndUpdate" reqest, err := http.NewRequest("POST", url, strings.NewReader(data)) ``` 最常见的一种情况是发送一个json文件过去,可以把Header的类型设置成为: ``` "Content-Type", "application/json; charset=utf-8" ``` 其余的部分按照先前同样的方式进行设置发送提交就好。 生成的response结果的处理 一般在client构建好之后,要采用`client.Do(request)`方法提交client请求,之后会返回一个*Response类型。response中的参数一般也比较多,我们需要的最多的通常是Body参数,一般通过body, _ := ioutil.ReadAll(resp.Body)会把body转化为[]byte类型返回过来, 之后再进行其他的处理。 # 参考 http://www.01happy.com/golang-http-client-get-and-post/ https://studygolang.com/articles/4489

golang笔记---template模板语法

[TOC] # 基本语法 - g使用 `{{` 和 `}}` 作为左右标签 - 使用 . 来访问当前位置的上下文 - 使用 $ 来引用当前模板根级的上下文 - 使用 $var 来访问创建的变量 # macaron 中控制器传递参数给模板 ``` type WebSite struct{ Title string Url string } type User map[string]string func portal(ctx *macaron.Context) { WebSite := WebSite{"百度","www.baidu.com"} user := User{} user["Name"] = "archer" user["Age"] = "18" pages := []struct { Num int }{{10}, {20}, {30}} ctx.Data["Name"] = name ctx.Data["User"] = user ctx.Data["WebSite"] = WebSite this.Data["Total"] = 100 ctx.Data["Pages"] = pages ctx.HTML(200, "portal") } ``` # 变量 ``` {{ .Name }} //我们有时候需要定义变量,比如我们需要定义一个article变量,同时将其初始化为”hello”,那么我们可以这样写: {{$article := "hello"}} //假设我们想要把传入值的内容赋值给article,则可以这样写: {{$article := .ArticleContent}} //获取到这个变量的内容 {{ $article }} ``` # 结构体 ``` {{.WebSite.Title}} {{.WebSite.Url}} ``` # map类型 ``` {{ .User.Name }} {{ .User.Age }} ``` # 遍历range 支持的类型为 array, slice, map, channel,对应的值长度为 0 时,range 不会执行 ``` //使用 .Num 输出子元素的 Num 属性,使用 $. 引用模板中的根级上下文 {{range .Pages}} {{.Num}} of {{$.Total}} {{end}} //使用创建的变量,在这里和 go 中的 range 用法是相同的。 {{range $index, $elem := .Pages}} {{$index}} - {{$elem.Num}} - {{.Num}} of {{$.Total}} {{end}} //range 也支持 else {{range .Pages}} {{else}} {{/* 当 .Pages 为空 或者 长度为 0 时会执行这里 */}} {{end}} ``` # 判断 当.condition为bool类型的时候,则为true表示执行,当.condition为string类型的时候,则非空表示执行。 ``` {{ if .Condition }} <h1>logged in</h1> {{ end }} ``` ``` {{ if not .Condition }} <h1>Not logged in</h1> {{ end }} ``` 支持else, else if ``` {{if .condition1}} {{else if .contition2}} {{end}} ``` 当进行逻辑判断的时候,需要一些内置模板函数 ``` //not 非 {{if not .condition}} {{end}} //and 与 {{if and .condition1 .condition2}} {{end}} //or 或 {{if or .condition1 .condition2}} {{end}} //eq 等于 {{if eq .var1 .var2}} {{end}} //ne 不等于 {{if ne .var1 .var2}} {{end}} //lt 小于 (less than) {{if lt .var1 .var2}} {{end}} //le 小于等于 {{if le .var1 .var2}} {{end}} //gt 大于 {{if gt .var1 .var2}} {{end}} //ge 大于等于 {{if ge .var1 .var2}} {{end}} ``` # with封装 with语言在Python中可以开启一个上下文环境。对于go模板,with语句类似,其含义就是创建一个封闭的作用域,在其范围内,可以使用.action,而与外面的.无关,只与with的参数有关: ``` {{ with arg }} 此时的点 . 就是arg {{ end }} ``` 也可以对变量赋值操作 ``` {{with $value := "My name is %s"}} {{printf . "slene"}} {{end}} ``` with 也支持 else ``` {{with pipeline}} {{else}} {{/* 当 pipeline 为空时会执行这里 */}} {{end}} ``` # 管道符 通过定义函数过滤器,实现模板的一些简单格式化处理。并且通过管道哲学,这样的处理方式可以连成一起。 ``` {{ p1 | p2 | p3 }} ``` 例如 模板内置了一些函数,比如格式化输出: ``` {{ 12.3456 | printf "%.2f" }} ``` # 函数 在 macaron中我们可以定义一个GetFirstChar方法,然后可以在模板中调用 ``` m := macaron.Classic() m.Use(macaron.Renderer(macaron.RenderOptions{ // 模板文件目录,默认为 "templates" Directory: "templates", // 模板文件后缀,默认为 [".tmpl", ".html"] Extensions: []string{".tmpl", ".html"}, // 模板函数,默认为 [] Funcs: []template.FuncMap{map[string]interface{}{ "AppName": func() string { return "Macaron" }, "GetFirstChar": func(str string) string { subStr := []rune(str) return string(subStr[0:1]) }, }}, // 模板语法分隔符,默认为 ["{{", "}}"] Delims: macaron.Delims{"{{", "}}"}, //... })) ``` 调用方法 ``` //可以直接调用,方法+参数 {{GetFirstChar "Archer"}} //也可以通过使用管道符的方式 {{"Archer"|GetFirstChar }} ``` # xss过滤 go模板会自动帮我们过滤 一些字符,以保障xss安全 如果我们想不转义,则可以使用下面方法 ``` ctx.Data["Title"] = template.HTML("<h1>这个标题不转义</h1>") ``` # 模板的嵌套 当模板想要引入子模板的时候,我们使用以下语句: ``` {{template "navbar"}} ``` 这样子就会尝试载入名称为navbar的子模板,同时我们也得定义一个子模板来实现”navbar”这个子模板。 子模板的定义为: ``` {{define "navbar"}} {{end}} ``` 在定义之间的内容将会覆盖`{{template “navbar”}}` 当然子模板是分离了,那么子模板能否获得父模板的变量呢?这是当然的,我们只需要使用 ``` {{template "navbar" .}} ``` 就可以将当前的变量传给子模板了,这个也是相当方便的。 # 参考: https://blog.csdn.net/huwh_/article/details/77140664 https://studygolang.com/articles/8023 https://www.jianshu.com/p/05671bab2357