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

1,387 阅读11分钟

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

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

原文在我的博客中:github.com/fortheallli…

欢迎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指针类型。