golang学习笔记(一)——golang基础和相关数据结构

53次阅读

共计 6234 个字符,预计需要花费 16 分钟才能阅读完成。

小白前端一枚,最近在研究 golang,记录自己学习过程中的一些笔记,以及自己的理解。

go 中包的依赖管理
go 中的切片
byte 和 string
go 中的 Map
go 中的 struct 结构体
go 中的方法
go 中的 interface 接口
interface{}

原文在我的博客中:https://github.com/forthealll…
欢迎 star~
1、go 中包的依赖管理
首先要了解的是 GOPATH 的含义,GOPATH 是 go 命令依赖的重要变量,可以通过:
go env

来查看相应的开发环境中 GOPATH 的值, 也可以通过 export GOPATH 指定:
export GOPATH = /usr/local/go

指定 GOPATH 目录后,GOPATH 目录包含了 3 个子目录:

src 存放源代码(比如.go、.h 文件等)
pkg 编译时生成的中间文件
bin 编译后生成的可执行文件

此外,go 的依赖管理中提供了 3 个主要的命令 go build、go get 和 go install。

go build: 会编译目录下或者指定的.go 文件, 得到一个可执行的文件
go install: 只能在 GOPATH 目录下使用,与 go build 大致相同会编译目录下执行的.go 文件,此外 go install 还会将可执行文件或库文件安装到 GOPATH 目录下。
go install + 远程地址: 会将相应的代码下载到 GOPATH 同时会编译该远程包
go get + 远程地址: 跟 go install+ 远程地址相同,会下载且编译
go get u + 远程地址: 下载并更新相应的远程地址的包,但不会自动编译

典型的例子,比如下载一个 dep 包:
go get -u github.com/golang/dep/cmd/dep

上述的 go get 和 go install + 远程包的方式,不能应用于需要版本管理依赖等场景, 可以通过安装 dep 包,来实现依赖管理。dep 提供了几个常用的命令,分别用于安装和更新相应的 go 包。

dep init 初始化一个项目依赖
dep ensure 安装项目所依赖的所有包
dep ensure -update 更新项目中的所有包
dep ensure -add github.com/pkg/errors 为项目添加单个依赖包

此外通过 Gopkg.toml 里面可以指定所依赖包的 git 分支,版本号等等,且在 dep ensure -add 中也可以指定分支和版本号,比如:
dep ensure -add github.com/pkg/foo@^1.0.1

提到包(package), 必须补充一句,在 go 中如果在其他包中引用变量,是通过:
包名. 变量名

的形式,在这里变量名必须是大写的,也就是说在 go 的包中,变量能否导出是根据变量的大小写来确定的,普遍认为如果变量是大写的就是在包内导出的,如果是变量小写的就是默认是包的私有变量。
2、go 中的切片
在 go 的函数调用中,如果传递的参数是一个较大的数组,显然如果直接将数组作为实参传入,在执行函数的过程中,实际上会拷贝一份该数组,会造成内存的浪费等。标准的做法,是传入数组的指针,或者对于数组的部分引用。
这里关于数组的部分引用,就是 slice 切片
(1)、go 中的切片简介
数组和切片之间存在着紧密的联系,slice 提供了访问数组子序列的功能。所谓的切片是对于数组的部分引用,slice 由三部分组成指针、长度和容量。

指针:指向第一个 slice 元素所对应的数组元素的地址
长度:slice 中元素的数目
容量:slice 中最多可容纳元素的数目

切片的定义方式:
var slice1 []type = make([]type, len, cap)

分别指定切片的类型,长度以及容量。
切片的初始化:
s := [] int { 1,2,3}

或者通过已经存在的数组来实现切片的初始化,
arr = [10]int {1,2,3,4,5,6,7,8,9,10}
s:=arr[1:5] // arr[startIndex:endIndex]

(2)、go 中的切片注意点
go 中的 slice 切片有一个注意点,就是如何判断切片为空,边界情况大致如下所示:
var s []int //len(s)==0,s==nil
s = nil //len(s)==0,s==nil
s = []int(nil)//len(s)==0,s==nil
s = []int{} //len(s)==0,s!=nil

显然如果通过 s ==nil 来判断,不能区别第四种场景,因此判断切片为空的正确方式是 len(s)==0.
3、byte 和 string
下述的方法将返回一个 byte 的切片:
var test:= []byte(“hello”)

go 遍历 slice 动态删除 map 遍历删除安全.
4、go 中的 Map
map 是一个无序的 key/value 对的集合,其中在每一个 map 中 key 是唯一的。go 中的 map 只要坑在于 map 是无序的。
(1)、Map 简介
声明一个 map:
var ages map[string]int // 同样初始的情况下,ages = nil
ages == nil // true

如果声明了但是没有赋值,那么尝试插入一对 key/value 会报错,比如上述声明但没有初始化的情况下:
age[“jony”] = 25 // 会 panic

解决方法,就是给 age 定义后赋值:
ages = make(map[string]int)

或者定义的时候同时赋值:
ages := map[string]int{

}
此后插入不存在的 key/value 就不会报错。
注意:尝试从 map 中去一个不存在的 key,默认的 value 值为 0
(2)、Map 无序性
我们从 map 的遍历结果,来说明 map 是无序的。比如我们以这么一个 map 为例:
var ages = map[string]int{
“a”:21,
“b”:22,
“c”:23,
};
for name,age := range ages {
fmt.Printf(“%s\t%d\n”,name,age);
}
通过 for range 可以遍历 map 对象,分别执行三次遍历后,来看遍历的结果

第一次输出:c 23 a 21 b 22

第二次输出:c 23 b 22 a 21
第三次输出: a 21 b 22 c 23

从上述的结果我们也可以看出 map 的每次遍历的结果都是不确定的。
注意:Map 的 value 类型不仅仅可以是基本类型,也可以是聚合类型,比如 map 或者 slice。
5、go 中的 struct 结构体
跟 C ++ 中的结构体类似,go 中的结构体是一种聚合数据类型,由 0 个或者多个任意值聚合成实体。
(1)、结构体简介
声明一个结构体很简单,比如我们声明了一个 Person 结构体:
type Person struct {
name string
age int
salary int
}

然后可以声明一个 Person 类型的变量:
var person Person

然后可以通过点操作符访问和赋值。
person.age = 25

此外,可以通过取地址符号加点操作符来访问和赋值,下述取地址的方式效果与上述是相同的。
(&person).age = 25

此外,结构体也支持嵌套。
6、go 中的方法
在 go 中没有明确的定义类,但是可以将结构体 struct 来类比其他语言中的 class。
go 中的方法与结构体相关,为了说名 go 中的方法,我们先从 go 中的函数讲起。
(1)、go 中的函数简介
在 go 中函数声明包括函数名、形参列表、返回值列表 (可省略 不傲视无返回值) 以及函数体。
func name (parameter-list)(result-list){

}

比如我们有一个 count 函数可以如此简单的定义:
func count(x,y int) int {
return x + y
}

(2)、go 中方法简介
在函数定义的基础上我们来介绍一下,如何定义方法。在函数声明时,在函数名前放上一个变量,这个变量称为方法的接收器,一般是结构体类型的。
当然也不一定是结构体,基本类型数值、字符串、slice 和 map 上面都可以作为接收器来定义方法。
声明方法的方式具体可以如下所示:
func (receive Receive) name(parameter-list)(result-list){

}

从上述的声明中也可以看出来只不过在函数的技术上增加了第一个参数接收器,为相应的接收器增加了该名称的方法。比如我们定一个 Person 结构体,并为其声明 sellHello 方法:
type Person struct {
name string
age int
salary int
}

func (person Person) sayHello() string{
return “Hello “+ person.name
}

p := Person{
name: “Jony”,
age: 25,
salary:100
}

fmt.Println(p.sayHello());// 输出 Hello Jony

上述就是在结构体 Person 上定义了一个 sayHello 方法,在结构体被初始化后,可以通过 p.sayHello()的方式直接调用。
除此之外,我们前面将到定义方法时的接收器不一定是一个结构体,接收器也可以接受基本类型等,比如:
type Mystring string;

func (mystring Mystring)sayHello() string{
return “Hello”+ string(mystring);
}

var m Mystring
m = “Jony”
fmt.Println(m.sayHello());

上述的例子同样会输出 Hello Jony.
甚至 nil 也可以作为方法的接收器,这里就不具体举例。
(3)、基于指针对象的方法
在函数调用时,是对实参的一个拷贝,如果函数需要更新一个变量,或者传递的参数过大,默认拷贝太为负责,我们经常会使用指针的形式,对于方法而言也同样如此,也就是说方法的接收器可以是指针类型。
对比于上述非指针类型的方法,声明指针类型的方法具体如下所示:
func (receive *Receive) name(parameter-list)(result-list){

}

指针类型的参数作为接收器,可以修改传入参数的实际变量的值。
type Person struct {
name string
age int
salary int
}
func (person *Person) changeAge(newAge int){
(*person).age = newAge
}
p.changeAge(30);
fmt.Println(p.age); // 输出了 30, 发现 age 确实发生了改变。

7、go 中的 interface 接口
我们前面也说过 go 不是一种传统的面向对象的语言,没有类和继承的概念,go 里面通过 interface 接口可以实现很多面向对象的特性。
接口的通俗定义:
接口提供了一种方式来说明对象的行为,接口定义了一组方法,但是不包含实现。
(1)、interface 接口简介
可以通过如下格式来定义接口:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type

}
go 中的接口都很简短,一般包含了 0 - 3 个方法。
同时我们可以通过:
var ai Namer

来定义一个接口类型的变量,初始值为 nil. 接口类型的变量是一个指针,声明而未赋值的情况下就为 nil。
go 中的接口有以下需要注意的点:

一个类型可以实现多个接口
接口类型可以包含一个实例的引用,该实例的类型实现了此接口
即使接口在类型之后定义,二者存在不同的包中,被单独编译,但是只要类型实现了接口中的方法,它就实现了此接口
实现某个接口的类型,除了实现接口方法外,还可以有其他的方法

上述几点都比较好理解,具体第二点,举例来说:
type Person struct {
name string
age int
salary int
}
type Say interface {
sayHello() string
}
func (person Person) sayHello() string {
return “Hello “+person.name
}

func main() {
p := new(Person)
p.name = “Jony”

var s Say;
s = p;
fmt.Println(s)
}

上述例子中,我们首先 new 了一个 Person 结构体类型的变量,并赋值给 p,因为 Person 接口体中实现了 Say 接口中的所有方法 sayHello 等。因此我们就说 Person 实现了 Say 接口,因此 Person 的实例 p,可以赋值给一个 Say 接口类型的变量 s。
此时的 s 是个指针,指向 Person 结构体实例 p。
(2)、interface 接口类型断言
任何类型只要实现了接口中的所有方法,我们就说该类型实现了该接口。这样一个接口类型的变量 varI 可以包含任何类型的值,在 go 中提供了一种安全的方式来检测它的动态类型。
if v,ok := varI.(T);ok {
Process(v)
return
}

如果转化合法,那么 v 是 varI 转化到类型 T 的值,ok 会是 true,否则 v 是类型 T 的零值,ok 是 false。这是一种安全的转化方式不会有错误发生。
我们还是接着上面的代码来讲我们的例子:
type Person struct {
name string
age int
salary int
}

type Say interface {
sayHello() string
}

func (person Person) sayHello() string {
return “Hello “+person.name
}

func main() {
p := new(Person)
p.name = “Jony”

var s Say;
s = p;
if t,ok := s.(*Person);ok {
fmt.Printf(“The type of s is:%T\n”,t);
}
}

输出的结果为 The type of s is:*main.Person。也可以使用特殊的 type-switch 来判断。
switch t:= s.(*Person){
case *Person:
fmt.Printf(“The type of s is:%T\n”,t);
case nil:

default:

}

8、interface{}
interface{}是一个空接口, 任何类型的值都可以复制给 interface{}类型的变量。
比如,我们首先声明一个类型为 interface{}的变量:
var test interface{}

任意类型的值都可以复制给 test, 比如下列基本类型的值复制给 test 是有效的:
var test interface{}
test = 1
test = true
test =”Hello”

此外,复杂的派生类型也可以赋值给 test,我们以指针类型举例:
var test interface{}
var a = 1
test = &a

interface 类型的变量是没有类型的,但是我们可以人为的进行类型转换:
var test interface{}
var a string
test = “hello”
a = test.(string)

上述,可以将 test 转化成 string 类型,这样就可以赋值给 string 类型变量 a 了。通过.(类型名)的方法可以将 interface{}类型的变量转化成任意的类型。
最后举一个简单的例子:
func main() {
a := make([]interface{},10)
b :=1
a[1]=&b
fmt.Println(*(a[1].(*int)))
}
上述代码发现,将 interface{}类型切片中的某一元素的值复制给了 int 指针类型,然后进行了类型转化,将 interface{}类型的变量转换成了 int 指针类型。

正文完
 0