GO系列—基础知识指南

Wesley13
• 阅读 766

Go语言基础知识点

1. 说明

2. 环境安装

2.1. 配置的三个环境变量说明

  • go env命令可以查看Go语言的环境变量设置
  • GOROOT:这个是Go语言的安装目录
  • GOPATH:这个是Go语言的工作目录
    • GOPATH可以有多个用符号分割,(多个目录的时候Windows是分号,Linux系统是冒号)。但是第下载的包会放在第一个下面
    • 从go 1.8开始,GOPATH环境变量现在有一个默认值,如果它没有被设置。 它在Unix上默认为$HOME/go,在Windows上默认为%USERPROFILE%/go。
  • GOBIN:这个实际上指向的是%GOPATH%/bin目录

2.2. linux

  • 首先,根据对应的操作系统选择安装包下载,在这里我使用的是Centos 64位系统

wget https://studygolang.com/dl/golang/go1.9.2.linux-amd64.tar.gz

sudo tar -xzf go1.8.3.linux-amd64.tar.gz -C /usr/local

  • 配置 /etc/profile

vi /etc/profile

  • 添加环境变量GOROOT和将GOBIN添加到PATH中

export GOROOT=/usr/local/go

export PATH=$PATH:$GOROOT/bin

  • 添加环境变量GOPATH(这个可按实际情况设置目录位置)

export GOPATH=/usr/local/go/path

  • 配置完毕后,执行命令令其生效

source /etc/profile

  • 在控制台输入go version,若输出版本号则安装成功

2.3. windows

2.4. 目录结构介绍

首先,我们在解压的时候会得到一个名为go的文件夹,其中包括了所有Go语言相关的一些文件,在这下面又包含很多文件夹和文件,我们来简单说明其中主要文件夹的作为:

  • api:用于存放依照Go版本顺序的API增量列表文件。这里所说的API包含公开的变量、常量、函数等。这些API增量列表文件用于Go语言API检查
  • bin:用于存放主要的标准命令文件(可执行文件),包含go、godoc、gofmt
  • blog:用于存放官方博客中的所有文章
  • doc:用于存放标准库的HTML格式的程序文档。我们可以通过godoc命令启动一个Web程序展示这些文档
  • lib:用于存放一些特殊的库文件
  • misc:用于存放一些辅助类的说明和工具
  • pkg:用于存放安装Go标准库后的所有归档文件(以.a结尾的文件)。注意,你会发现其中有名称为linux_amd64的文件夹,我们称为平台相关目录。这类文件夹的名称由对应的操作系统和计算架构的名称组合而成。通过go install命令,Go程序会被编译成平台相关的归档文件存放到其中
  • src:用于存放Go自身、Go标准工具以及标准库的所有源码文件
  • test:存放用来测试喝验证Go本身的所有相关文件

2.5. 其他概念介绍

2.5.1. 工作区

这在Go中是一个非常重要的概念,在一般情况下,Go源码文件必须放在工作区中,也就是说,我们写的项目代码都必须放在我们所设定的工作区中,虽然对于命令源码文件来说,这不是必须的。但我们大多都是前一种情况。工作区其实就是一个对应特定工程的目录,它应包含3个子目录:

  • src目录:存放源代码(比如:.go .c .h .s等)
  • pkg目录:编译后生成的文件(比如:.a)
  • bin目录:编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个gopath,那么使用${GOPATH//://bin:}/bin添加所有的bin目录)

2.5.2. 命令源文件

如果一个源码文件被声明属于main代码包,且该文件代码中包含无参数声明喝结果声明的main函数,则它就是命令源码文件。命令源码文件可通过go run命令直接启动运行

2.5.3. .a文件

2.5.3.1. 编译应用

  • 上面我们已经建立了自己的应用包,如何进行编译安装呢?有两种方式可以进行安装

  • 1、只要进入对应的应用包目录,然后执行go install,就可以安装了

  • 2、在任意的目录执行如下代码go install mymath

    安装完之后,我们可以进入如下目录

    cd $GOPATH/pkg/${GOOS}_${GOARCH} //可以看到如下文件 mymath.a

    这个.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)) }

    可以看到这个的package是main,import里面调用的包是mymath,这个就是相对于$GOPATH/src的路径,如果是多级目录,就在import里面引入多级目录,如果你有多个GOPATH,也是一样,Go会自动在多个$GOPATH/src中寻找。

    如何编译程序呢?进入该应用目录,然后执行go build,那么在该目录下面会生成一个mathapp的可执行文件

    ./mathapp

    输出如下内容

    Hello, world. Sqrt(2) = 1.414213562373095

    如何安装该应用,进入该目录执行go install,那么在$GOPATH/bin/下增加了一个可执行文件mathapp, 还记得前面我们把$GOPATH/bin加到我们的PATH里面了,这样可以在命令行输入如下命令就可以执行

    mathapp

    也是输出如下内容

    Hello, world. Sqrt(2) = 1.414213562373095

    这里我们展示如何编译和安装一个可运行的应用,以及如何设计我们的目录结构。

2.5.4. bin和pkg目录干什么用的 ??????

  • 目前说不清楚

2.5.5. go build和go install还有go run,go get的区别

  • go run:go run 编译并直接运行程序,它会产生一个临时文件(但不会生成 .exe 文件),直接在命令行输出程序执行结果,方便用户调试。
  • go build:go build 用于测试编译包,主要检查是否会有编译错误,如果是一个可执行文件的源码(即是 main 包),就会直接生成一个可执行文件到当前的命名执行目录。
  • go install:go install 的作用有两步:第一步是编译导入的包文件,所有导入的包文件编译完才会编译主程序;第二步是将编译后生成的可执行文件放到 bin 目录下($GOPATH/bin),编译后的包文件放到 pkg 目录下($GOPATH/pkg)。
  • go get:go get会做两件事:
    • 从远程下载需要用到的包
    • 执行go install
  • 下面的链接是各命令的执行结果:https://blog.csdn.net/zyz770834013/article/details/78656985

2.5.6. GO命令介绍

3. 关键字

25个关键字

break    default      func    interface    select
case     defer        go      map          struct
chan     else         goto    package      switch
const    fallthrough  if      range        type
continue for          import  return       var


var和const Go语言基础里面的变量和常量申明
package和import已经有过短暂的接触
func 用于定义函数和方法
return 用于从函数返回
defer 用于类似析构函数
go 用于并发
select 用于选择不同类型的通讯
interface 用于定义接口
struct 用于定义抽象数据类型
break、case、continue、for、fallthrough、else、if、switch、goto、default这些参考2.3流程介绍里面
chan用于channel通讯
type用于声明自定义类型
map用于声明map类型数据
range用于读取slice、map、channel数据

4. 包

package是最基本的分发单位和工程管理中依赖关系的体现

4.1. 包概念

  • 多个文件可能被打包在一起,因为包名是一样的,有些文件里面的变量没有定义,但是可能在其他文件中定义了,只是包名一样,打包到一起了
  • 然后我们在go中是按照文件夹引用的
  • Foo 和 FOO 都是被导出的名称。名称 foo 是不会被导出的。大写的函数名是被导出的
  • 每个Go语言源代码文件开头都必须要有一个package声明,表示源代码文件所属包
  • 要生成Go语言可执行程序,必须要有名为main的package包,且在该包下必须有且只有一个main函数
  • 同一个路径下只能存在一个package,一个package可以由多个源代码文件组成

4.2. 包引用原理

  • 跟package类似,import原理遵守以下几个原则:
  • 如果一个main导入其他的包,包会被顺序导入
  • 如果导入的包(pkg1)依赖其他的包(包pkg2),会首先导入pkg2,然后初始化pkg2中的常量与变量,如果pkg2中有init函数,会自动执行init
  • 所有包导入完成后才会对main的常量和变量进行初始化,然后执行main中的init函数(如果有的话),最后执行main函数
  • 如果一个包被导入多次实际上只会导入一次

4.3. import

4.3.1. 普通引入

  • 相对路径:import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import

  • 绝对路径:import “shorturl/model” //加载gopath/src/shorturl/model模块

    import "fmt"
    
    inport (
        "fmt"
    )
    
    import (
        "log"
        "fmt"
    
        "github.com/jinzhu/gorm"
        _ "github.com/jinzhu/gorm/dialects/mysql"
    
        "gin-blog/pkg/setting"
    )
    

4.3.2. 特殊用法

  • 1、.操作:这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")

    import( . "fmt" )

  • 2、别名操作:别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

    import( f "fmt" )

  • 3、_操作:_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数

    import ( "database/sql" _ "github.com/ziutek/mymysql/godrv" )

5. 变量

5.1. 变量定义

  • 函数外的每个语句都必须以关键字开始(varfunc、等等)

  • 在函数中,:= 简洁赋值语句在明确类型的地方,可以用于替代 var 定义。

  • := 这种形式有个局限只能用在函数内部,所以一般用var方式来定义全局变量

  • _(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。

  • 对于没有给类型的变量,Go会根据其相应值的类型推倒来帮你初始化它们

  • Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i但未使用。

    package main

    func main() { var i int }

5.2. 变量赋值

var variableName type
var vname1, vname2, vname3 type
var variableName type = value
var vname1, vname2, vname3 type= v1, v2, v3
// Go会根据其相应值的类型来帮你初始化它们
var vname1, vname2, vname3 = v1, v2, v3
vname1, vname2, vname3 := v1, v2, v3

5.3. 分组声明

  • 在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。

    import "fmt" import "os"

    const i = 100 const pi = 3.1415 const prefix = "Go_"

    var i int var pi float32 var prefix string

    可以分组写成如下形式:

    import( "fmt" "os" )

    const( i = 100 pi = 3.1415 prefix = "Go_" )

    var( i int pi float32 prefix string )

5.4. 常量

  • 所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。

  • Go常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位), 若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit

  • 常量不能使用 := 语法定义。

  • 常量通常不需要指定类型,如果需要,也可以明确指定常量的类型

    const constantName = value const Pi float32 = 3.1415926

5.5. 零值

  • 变量在定义时没有明确的初始化时会赋值为_零值_。

  • 关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。

  • 零值是

    int 0 int8 0 int32 0 int64 0 uint 0x0 rune 0 //rune的实际类型是 int32 byte 0x0 // byte的实际类型是 uint8 float32 0 //长度为 4 byte float64 0 //长度为 8 byte bool false string ""

6. 内置基础类型

6.1. 基础类型

  • bool

  • string

    • 字符串是用一对双引号("")或反引号(`)括起来定义

    • 在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0]

      var s string = "hello" s[0] = 'c'

    • 但如果真的想要修改怎么办呢?下面的代码可以实现

      s := "hello"
      

      c := []byte(s) // 将字符串 s 转换为 []byte 类型 c[0] = 'c' s2 := string(c) // 再转换回 string 类型 fmt.Printf("%s\n", s2)

    • Go中可以使用+操作符来连接两个字符串

      s := "hello," m := " world" a := s + m fmt.Printf("%s\n", a)

    • 修改字符串也可写为

      s := "hello"
      

      s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 fmt.Printf("%s\n", s)

    • 声明多行字符串,可以通过`来声明, 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出

      m := hello world

  • 整数类型

    • 无符号:int int8 int16 int32 int64
    • 带符号:uint uint8 uint16 uint32 uint64 uintptr
    • 其中rune是int32的别称,byte是uint8的别称
    • 这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。
  • 浮点型

    • float32 float64
    • (没有float类型),默认是float64
  • 复数

    • complex64 complex128
    • 它的默认类型是complex128(64位实数+64位虚数)。如果需要小一些的,也有complex64(32位实数+32位虚数)。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位
  • 错误类型

    • Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误

      err := errors.New("emit macho dwarf: elf header corrupted")
      

      if err != nil { fmt.Print(err) }

6.2. 类型转换

表达式 T(v) 将值 v 转换为类型 T

一些关于数值的转换:

var i int = 42 var f float64 = float64(i) var u uint = uint(f)

或者,更加简单的形式:

i := 42 f := float64(i) u := uint(f)

与 C 不同的是 Go 的在不同类型之间的项目赋值时需要显式转换。 试着移除例子中 float64 或 int 的转换看看会发生什么。

6.3. 类型推倒

在定义一个变量但不指定其类型时(使用没有类型的 var 或 := 语句), 变量的类型由右值推导得出。

当右值定义了类型时,新变量的类型与其相同:

var i int j := i // j 也是一个 int

但是当右边包含了未指名类型的数字常量时,新的变量就可能是 int 、 float64 或 complex128。 这取决于常量的精度:

i := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128

尝试修改演示代码中 v 的初始值,并观察这是如何影响其类型的。

7. 复杂数据类型

7.1. iota枚举

<http://www.cnblogs.com/zsy/p/5370052.html>
Go里面有一个关键字iota,这个关键字用来声明enum的时候采用,它默认开始值是0,const中每增加一行加1:

package main

import (
    "fmt"
)

const (
    x = iota // x == 0
    y = iota // y == 1
    z = iota // z == 2
    w        // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)

const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0

const (
    h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)

const (
    a       = iota //a=0
    b       = "B"
    c       = iota             //c=2
    d, e, f = iota, iota, iota //d=3,e=3,f=3
    g       = iota             //g = 4
)

func main() {
    fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}

    除非被显式设置为其它值或iota,每个const分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是iota,则它也被设置为iota。

7.2. 指针

Go 具有指针。 指针保存了变量的内存地址。

类型 *T 是指向类型 T 的值的指针。其零值是 nil

var p *int int型的指针

& 符号会生成一个指向其作用对象的指针。

i := 42 p = &i

  • 符号表示指针指向的底层的值。

fmt.Println(p) // 通过指针 p 读取 i p = 21 // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“非直接引用”。

与 C 不同,Go 没有指针运算。

package main

import "fmt"

func main() { i, j := 42, 2701

p := &i         // point to i
fmt.Println(*p) // read i through the pointer
*p = 21         // set i through the pointer
fmt.Println(i)  // see the new value of i

p = &j         // point to j
*p = *p / 37   // divide j through the pointer
fmt.Println(j) // see the new value of j

}

7.3. 结构体

Go语言中,也和C或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之struct

7.3.1. 声明

  • 普通

    type person struct { name string age int }

    var P person // P现在就是person类型的变量了

    P.name = "Astaxie" // 赋值"Astaxie"给P的name属性. P.age = 25 // 赋值"25"给变量P的age属性 fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.

  • 按照顺序提供初始化值:P := person{"Tom", 25}

  • 通过field:value的方式初始化,这样可以任意顺序:P := person{age:24, name:"Tom"}

  • 当然也可以通过new函数分配一个指针,此处P的类型为*person:P := new(person)

7.3.2. struct的匿名字段(嵌入字段)

  • 当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct

    package main

    import "fmt"

    type Human struct { name string age int weight int }

    type Student struct { Human // 匿名字段,那么默认Student就包含了Human的所有字段 speciality string }

    func main() { // 我们初始化一个学生 mark := Student{Human{"Mark", 25, 120}, "Computer Science"} // 我们访问相应的字段 fmt.Println("His name is ", mark.name) fmt.Println("His age is ", mark.age) fmt.Println("His weight is ", mark.weight) fmt.Println("His speciality is ", mark.speciality) // 修改对应的备注信息 mark.speciality = "AI" fmt.Println("Mark changed his speciality") fmt.Println("His speciality is ", mark.speciality) // 修改他的年龄信息 fmt.Println("Mark become old") mark.age = 46 fmt.Println("His age is", mark.age) // 修改他的体重信息 fmt.Println("Mark is not an athlet anymore") mark.weight += 60 fmt.Println("His weight is", mark.weight) }

  • 嵌入字段也可以作为字段名去使用

    mark.Human = Human{"Marcus", 55, 220} mark.Human.age -= 1

  • 通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的

    package main

    import "fmt"

    type Skills []string

    type Human struct { name string age int weight int }

    type Student struct { Human // 匿名字段,struct Skills // 匿名字段,自定义的类型string slice int // 内置类型作为匿名字段 speciality string }

    func main() { // 初始化学生Jane jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"} // 现在我们来访问相应的字段 fmt.Println("Her name is ", jane.name) fmt.Println("Her age is ", jane.age) fmt.Println("Her weight is ", jane.weight) fmt.Println("Her speciality is ", jane.speciality) // 我们来修改他的skill技能字段 jane.Skills = []string{"anatomy"} fmt.Println("Her skills are ", jane.Skills) fmt.Println("She acquired two new ones ") jane.Skills = append(jane.Skills, "physics", "golang") fmt.Println("Her skills now are ", jane.Skills) // 修改匿名内置类型字段 jane.int = 3 fmt.Println("Her preferred number is", jane.int) }

  • 嵌入字段属性名和原始属性名重名处理规则是:最外层的优先访问,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问

    package main

    import "fmt"

    type Human struct { name string age int phone string // Human类型拥有的字段 }

    type Employee struct { Human // 匿名字段Human speciality string phone string // 雇员的phone字段 }

    func main() { Bob := Employee{Human{"Bob", 34, "777-444-XXXX"}, "Designer", "333-222"} fmt.Println("Bob's work phone is:", Bob.phone) // 如果我们要访问Human的phone字段 fmt.Println("Bob's personal phone is:", Bob.Human.phone) }

7.3.3. 结构体指针????????

  • 通过指针间接的访问是透明的,其实我不是很明白这句话是做什么的

    package main

    import "fmt"

    type Vertex struct { X int Y int }

    func main() { v := Vertex{1, 2} p := &v p.X = 1e9 fmt.Println(v) } 结构体字段可以通过结构体指针来访问。

7.3.4. StructTag

7.3.4.1. 来源

  • 要先了解一下golang的基础,在golang中,命名都是推荐都是用驼峰方式,并且在首字母大小写有特殊的语法含义:包外无法引用。但是由经常需要和其它的系统进行数据交互,例如转成json格式,存储到mongodb啊等等。这个时候如果用属性名来作为键值可能不一定会符合项目要求。

  • 所以呢就多了小米点的内容,在golang中叫标签(Tag),在转换成其它数据格式的时候,会使用其中特定的字段作为键值

    u := &User{UserId: 1, UserName: "tony"} j, _ := json.Marshal(u) fmt.Println(string(j)) // 输出内容:{"user_id":1,"user_name":"tony"}

    如果在属性中不增加标签说明,则输出:

    {"UserId":1,"UserName":"tony"}

    可以看到直接用struct的属性名做键值。

    其中还有一个bson的声明,这个是用在将数据存储到mongodb使用的。

7.3.4.2. 语法

  • "号的貌似是注释
  • `号的貌似是转换的别名,和一些第三方的特性,通过反射实现

7.3.4.3. 取值

t := reflect.TypeOf(u)
field := t.Elem().Field(0)
fmt.Println(field.Tag.Get("json"))
fmt.Println(field.Tag.Get("bson"))


 1 package main
 2 import (
 3     "fmt"
 4     "reflect" // 这里引入reflect模块
 5 )
 6 type User struct {
 7     Name   string "user name" //这引号里面的就是tag
 8     Passwd string "user passsword"
 9 }
10 func main() {
11     user := &User{"chronos", "pass"}
12     s := reflect.TypeOf(user).Elem() //通过反射获取type定义
13     for i := 0; i < s.NumField(); i++ {
14         fmt.Println(s.Field(i).Tag) //将tag输出出来
15     }
16 }



 1 package main
 2  
 3 import (
 4     "fmt"
 5     "reflect"
 6 )
 7  
 8 func main() {
 9     type S struct {
10         F string `species:"gopher" color:"blue"`
11     }
12  
13     s := S{}
14     st := reflect.TypeOf(s)
15     field := st.Field(0)
16     fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
17  
18 }

7.4. 数组

  • var arr [n]type:在[n]type中,n表示数组的长度,type表示存储元素的类型

    package main
    import "fmt"
    func main() {
        var a [2]string
        a[0] = "Hello"
        a[1] = "World"
        fmt.Println(a[0], a[1])
        fmt.Println(a)
    }
    
  • 由于长度也是数组类型的一部分,因此[3]int与[4]int是不同的类型

  • 数组的长度不能改变

  • 数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针,如果要使用指针,那么就需要用到后面介绍的slice类型了

  • 数组可以使用另一种:=来声明

    a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
    b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0
    c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度☆☆☆☆☆
    
  • Go支持嵌套数组,即多维数组

    // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}

    // 上面的声明可以简化,直接忽略内部的类型 easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

7.5. 切片

7.5.1. 定义

  • 在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫slice

  • slice的声明也可以像array一样,只是不需要长度。

    package main
    
    import "fmt"
    
    func main() {
        p := []int{2, 3, 5, 7, 11, 13}
        fmt.Println("p ==", p)
    
        for i := 0; i < len(p); i++ {
            fmt.Printf("p[%d] == %d\n", i, p[i])
        }
    }
    

7.5.2. 对数组和silce切片

  • slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。

  • slice的默认开始位置是0,ar[:n]等价于ar[0:n]

  • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]

  • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]

    // 声明一个数组 var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} // 声明两个slice var aSlice, bSlice []byte

    // 演示一些简便操作 aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素

    // 从slice中获取slice aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7 bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g

7.5.3. nil slice

  • slice 的零值是 nil

  • 一个 nil 的 slice 的长度和容量是 0。

    package main
    
    import "fmt"
    
    func main() {
        var z []int
        fmt.Println(z, len(z), cap(z))
        if z == nil {
            fmt.Println("nil!")
        }
    }
    

7.5.4. slice是引用类型

  • slice并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层array
  • slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlice和bSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。
  • slice像一个结构体,这个结构体包含了三个元素
    • 一个指针,指向数组中slice指定的开始位置
    • 长度,即slice的长度
    • 最大长度(容量),也就是slice开始位置到数组的最后位置的长度
  • slice内置的append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响。

7.5.5. 构造slice ????????

slice 由函数 make 创建。这会分配一个零长度的数组并且返回一个 slice 指向这个数组:

a := make([]int, 5) // len(a)=5

为了指定容量,可传递第三个参数到 make

b := make([]int, 0, 5) // len(b)=0, cap(b)=5

b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=4

package main

import "fmt"

func main() {
    a := make([]int, 5)
    printSlice("a", a)
    b := make([]int, 0, 5)
    printSlice("b", b)
    c := b[:2]
    printSlice("c", c)
    d := c[2:5]
    printSlice("d", d)
}

func printSlice(s string, x []int) {
    fmt.Printf("%s len=%d cap=%d %v\n",
        s, len(x), cap(x), x)
}

7.5.6. 三个参数????????

从Go1.2开始slice支持了三个参数的slice,之前我们一直采用这种方式在slice或者array基础上来获取一个slice

var array [10]int slice := array[2:4]

这个例子里面slice的容量是8,新版本里面可以指定这个容量

slice = array[2:4:7]

上面这个的容量就是7-2,即5。这样这个产生的新的slice就没办法访问最后的三个元素。

如果slice是这样的形式array[:i:j],即第一个参数为空,默认值就是0。

7.5.7. 向slice添加元素

  • 对于slice有几个有用的内置函数:

    • len 获取slice的长度

    • cap 获取slice的最大容量

    • copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数

    • append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice

      • append 的第一个参数 s 是一个类型为 T 的数组,其余类型为 T 的值将会添加到 slice。

      • 如果 s 的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。

          package main
        
          import "fmt"
        
          func main() {
              var a []int
              printSlice("a", a)
        
              // append works on nil slices.
              a = append(a, 0)
              printSlice("a", a)
        
              // the slice grows as needed.
              a = append(a, 1)
              printSlice("a", a)
        
              // we can add more than one element at a time.
              a = append(a, 2, 3, 4)
              printSlice("a", a)
          }
        
          func printSlice(s string, x []int) {
              fmt.Printf("%s len=%d cap=%d %v\n",
                  s, len(x), cap(x), x)
          }
        

7.5.8. 切片的扩容原理

  • append增加容量是按照如果容量不够把之前切片的容量乘以2,如果乘以2还不够就之前容量+1乘以2来递增的

7.6. range

for 循环的 range 格式可以对 slice 或者 map 进行迭代循环。

package main

import "fmt"

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
    for i, v := range pow {
        fmt.Printf("2**%d = %d\n", i, v)
    }
}

可以通过赋值给 _ 来忽略序号和值。 如果只需要索引值,去掉“, value”的部分即可。

package main

import "fmt"

func main() {
    pow := make([]int, 10)
    for i := range pow {
        pow[i] = 1 << uint(i)
    }
    for _, value := range pow {
        fmt.Printf("%d\n", value)
    }
}

7.7. map

7.7.1. 定义

  • map在使用之前必须用 make 而不是 new 来make初始化

  • 空map的值为 nil,并且不能赋值

    package main
    
    import "fmt"
    
    type Vertex struct {
        Lat, Long float64
    }
    
    var m map[string]Vertex
    
    func main() {
        m = make(map[string]Vertex)
        m["Bell Labs"] = Vertex{
            40.68433, -74.39967,
        }
        fmt.Println(m["Bell Labs"])
    }
    
  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取

  • map的长度是不固定的,也就是和slice一样,也是一种引用类型

  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制???

7.7.2. 修改map

// 1、初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }

// 2、插入元素
m[key] = elem

// 3、获得元素
elem = m[key]

// 4、map有两个返回值,第二个返回值,如果存在ok为true,如果不存在key,那么ok为false,并且 elem 是 map 的元素类型的零值
csharpRating, ok := rating["C#"]
if ok {
    fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
    fmt.Println("We have no rating associated with C# in the map")
}

// 5、删除元素
delete(rating, "C")  // 删除key为C的元素

7.7.3. make和new操作

  • make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。

  • 内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:

    new返回指针。

  • 内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。

    make返回初始化后的(非零)值。

  • 总结

    • make用于内建类型(只能用于创建map、slice 和channel)的内存分配。并且返回一个有初始值(非零)的T类型,而不是*T。
    • new用于各种类型的内存分配。new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:*new返回指针。

8. 流程控制

8.1. for循环

Go里面最强大的一个控制逻辑就是for,它既可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操作

8.1.1. 标准写法

package main

import "fmt"

func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 输出:sum is equal to 45

8.1.2. 省略

  • 我们可以忽略前置、后置语句为空。

    sum := 1 for ; sum < 1000; { sum += sum }

  • 其中;也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是while的功能

    package main

    import "fmt"

    func main() { sum := 1 for sum < 1000 { sum += sum } fmt.Println(sum) }

8.1.3. 结合range

  • for配合range可以用于读取slice和map的数据

    for k,v:=range map { fmt.Println("map's key:",k) fmt.Println("map's val:",v) }

  • 由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值

    for _, v := range map{ fmt.Println("map's val:", v) }

8.1.4. 死循环

  • 如果省略了循环条件,循环就不会结束,因此可以用更简洁地形式表达死循环。

    package main

    func main() { for { } }

8.2. if语句

  • if有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了

    if x > 10 { fmt.Println("x is greater than 10") } else { fmt.Println("x is less than 10") }

    // 计算获取值x,然后根据x返回的大小,判断是否大于10。 if x := computedValue(); x > 10 { fmt.Println("x is greater than 10") } else { fmt.Println("x is less than 10") }

    //这个地方如果这样调用就编译出错了,因为x是条件里面的变量 fmt.Println(x)

    if integer == 3 { fmt.Println("The integer is equal to 3") } else if integer < 3 { fmt.Println("The integer is less than 3") } else { fmt.Println("The integer is greater than 3") }

8.3. switch

8.3.1. 标准形式

switch sExpr {
case expr1:
    some instructions
case expr2:
    some other instructions
case expr3:
    some other instructions
default:
    other code
}

8.3.2. fallthrough

  • Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。

    integer := 6 switch integer { case 4: fmt.Println("The integer was <= 4") fallthrough case 5: fmt.Println("The integer was <= 5") fallthrough case 6: fmt.Println("The integer was <= 6") fallthrough case 7: fmt.Println("The integer was <= 7") fallthrough case 8: fmt.Println("The integer was <= 8") fallthrough default: fmt.Println("default case") }

8.3.3. 没有条件的switch

  • 没有条件的 switch 同 switch true 一样。

  • 这一构造使得可以用更清晰的形式来编写长的 if-then-else 链

    package main

    import ( "fmt" "time" )

    func main() { t := time.Now() switch { case t.Hour() < 12: fmt.Println("Good morning!") case t.Hour() < 17: fmt.Println("Good afternoon.") default: fmt.Println("Good evening.") } }

8.4. goto

  • Go有goto语句——请明智地使用它。用goto跳转到必须在当前函数内定义的标签。例如假设这样一个循环

  • 标签名是大小写敏感的

    func myFunc() {
      i := 0
    Here:   //这行的第一个词,以冒号结束作为标签
      println(i)
      i++
      goto Here   //跳转到Here去
    }
    

9. 函数

9.1. 定义

  • 函数是Go里面的核心设计,它通过关键字func来声明,它的格式如下

    func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { //这里是处理逻辑代码 //返回多个值 return value1, value2 }

  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔

  • 函数可以返回多个值

  • 上面返回值声明了两个变量output1和output2,如果你不想声明也可以,直接就两个类型

  • 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号

  • 如果没有返回值,那么就直接省略最后的返回信息

  • 如果有返回值, 那么必须在函数的外层添加return语句

    package main

    import "fmt"

    // 返回a、b中最大值. func max(a, b int) int { if a > b { return a } return b }

    func main() { x := 3 y := 4 z := 5 max_xy := max(x, y) //调用函数max(x, y) max_xz := max(x, z) //调用函数max(x, z) fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它 }

9.2. 函数空返回

  • 如下,函数没有返回值,但是外面add和multiplied已经可以用了,因为在返回的时候初始化了

    func SumAndProduct(A, B int) (add int, Multiplied int) { add = A+B Multiplied = A*B return }

9.3. 函数变参

  • Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:

    func myfunc(arg ...int) {}

  • arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个int的slice:

    for _, n := range arg { fmt.Printf("And the number is: %d\n", n) }

9.4. 传值与传指针

  • 当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。如果传指针,此时参数仍然是按copy传递的,只是copy的是一个指针。

  • 传指针使得多个函数能操作同一个对象。

  • 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。

  • Go语言中channel,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)

    package main

    import "fmt"

    //简单的一个函数,实现了参数+1的操作 func add1(a *int) int { // 请注意, *a = *a+1 // 修改了a的值 return *a // 返回新值 }

    func main() { x := 3 fmt.Println("x = ", x) // 应该输出 "x = 3" x1 := add1(&x) // 调用 add1(&x) 传x的地址 fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" fmt.Println("x = ", x) // 应该输出 "x = 4" }

9.5. defer

  • Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回

    • 自己的理解:按照上面这句话说的,defer一定会是在函数执行到最后,这个最后是函数马上要结束了,包括提前返回。然后执行defer语句, 再返回要返回的内容

    func ReadWrite() bool { file.Open("file") // 做一些工作 if failureX { file.Close() return false }

    if failureY {
        file.Close()
        return false
    }
    
    file.Close()
    return true
    

    }

    我们看到上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在defer后指定的函数会在函数退出前调用。

    func ReadWrite() bool { file.Open("file") defer file.Close() if failureX { return false } if failureY { return false } return true }

  • 如果有很多调用defer,那么defer是采用后进先出模式

    所以如下代码会输出4 3 2 1 0 for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }

  • 几篇文章

9.6. 函数的闭包

  • Go函数可以是闭包的。闭包是一个函数值,它来自函数体的外部的变量引用。 函数可以对这个引用值进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。

    例如,函数 adder 返回一个闭包。每个闭包都被绑定到其各自的 sum 变量上。

    package main import "fmt" func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) } }

9.7. 函数作为值、类型

  • 在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

    type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

  • 将函数作为参数进行传递,我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。

    package main

    import "fmt"

    type testInt func(int) bool // 声明了一个函数类型

    func isOdd(integer int) bool { if integer%2 == 0 { return false } return true }

    func isEven(integer int) bool { if integer%2 == 0 { return true } return false }

    // 声明的函数类型在这个地方当做了一个参数

    func filter(slice []int, f testInt) []int { var result []int for _, value := range slice { if f(value) { result = append(result, value) } } return result }

    func main(){ slice := []int {1, 2, 3, 4, 5, 7} fmt.Println("slice = ", slice) odd := filter(slice, isOdd) // 函数当做值来传递了 fmt.Println("Odd elements of slice are: ", odd) even := filter(slice, isEven) // 函数当做值来传递了 fmt.Println("Even elements of slice are: ", even) }

    函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。

9.8. Panic和Recover

  • Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。这是个强大的工具,请明智地使用它。

  • Panic:是一个内建函数,可以中断原有的控制流程,进入一个panic状态中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。panic可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

  • Recover:是一个内建的函数,可以让进入panic状态的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入panic状态,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

    下面这个函数演示了如何在过程中使用panic

    var user = os.Getenv("USER")

    func init() { if user == "" { panic("no value for $USER") } }

    下面这个函数检查作为其参数的函数在执行时是否会产生panic:

    func throwsPanic(f func()) (b bool) { defer func() { if x := recover(); x != nil { b = true } }() f() //执行函数f,如果f中出现了panic,那么就可以恢复回来 return }

9.9. main函数和init函数

  • Go里面有两个保留的函数:init函数(能够应用于所有的package)和main函数(只能应用于package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数。
  • Go程序会自动调用init()和main(),所以你不需要在任何地方调用这两个函数。每个package中的init函数都是可选的,但package main就必须包含一个main函数。
  • 程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。下图详细地解释了整个执行过程:

10. 方法(面向对象)

10.1. 用方法实现类

  • Go没有类。然而,仍然可以在结构体类型上定义方法。
  • 前面我们介绍了函数和struct,那你是否想过函数当作struct的字段一样来处理呢?今天我们就讲解一下函数的另一种形态,带有接收者的函数,我们称为method
  • 方法接收者 出现在 func 关键字和方法名之间的参数中。

10.2. 自定义类型

  • 实际上只是一个定义了一个类型别名

    type ages int

    type money float32

    type months map[string]int

    m := months { "January":31, "February":28, ... "December":31, }

10.3. 任意类型都可以定义方法

  • 任何你自定义的类型、内置类型、struct等各种类型上面

  • 但是,不能对来自其他包的类型或基础类型定义方法

    package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.Xv.X + v.Y*v.Y) } func main() { v := &Vertex{3, 4} fmt.Println(v.Abs()) }

    package main

    import ( "fmt" "math" )

    type MyFloat float64

    func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) }

    func main() { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs()) }

10.4. 接收者为指针的方法

  • 首先避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)

  • 其次,方法可以修改接收者指向的值

    package main

    import "fmt"

    const( WHITE = iota BLACK BLUE RED YELLOW )

    type Color byte

    type Box struct { width, height, depth float64 color Color }

    type BoxList []Box //a slice of boxes

    func (b Box) Volume() float64 { return b.width * b.height * b.depth }

    func (b *Box) SetColor(c Color) { b.color = c }

    func (bl BoxList) BiggestColor() Color { v := 0.00 k := Color(WHITE) for _, b := range bl { if bv := b.Volume(); bv > v { v = bv k = b.color } } return k }

    func (bl BoxList) PaintItBlack() { for i := range bl { bl[i].SetColor(BLACK) } }

    func (c Color) String() string { strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"} return strings[c] }

    func main() { boxes := BoxList { Box{4, 4, 4, RED}, Box{10, 10, 1, YELLOW}, Box{1, 1, 20, BLACK}, Box{10, 10, 1, BLUE}, Box{10, 30, 1, WHITE}, Box{20, 20, 20, YELLOW}, } fmt.Printf("We have %d boxes in our set\n", len(boxes)) fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³") fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String()) fmt.Println("The biggest one is", boxes.BiggestColor().String()) fmt.Println("Let's paint them all black") boxes.PaintItBlack() fmt.Println("The color of the second one is", boxes[1].color.String()) fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String()) }

10.4.1. 指针和非指针的区别

  • 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作
  • 也就是说:如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method
  • 类似的:如果一个method的receiver是T,你可以在一个_T类型的变量P上面调用这个method,而不需要_ P去调用这个method

10.5. method继承

  • method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method

    package main

    import "fmt"

    type Human struct { name string age int phone string }

    type Student struct { Human //匿名字段 school string }

    type Employee struct { Human //匿名字段 company string }

    //在human上面定义了一个method func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) }

    func main() { mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} mark.SayHi() sam.SayHi() }

10.6. method重写

  • 上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法

    package main

    import "fmt"

    type Human struct { name string age int phone string }

    type Student struct { Human //匿名字段 school string }

    type Employee struct { Human //匿名字段 company string }

    //Human定义method func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) }

    //Employee的method重写Human的method func (e *Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //Yes you can split into 2 lines here. }

    func main() { mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} mark.SayHi() sam.SayHi() }

11. 接口

11.1. interface类型

  • 接口类型是由一组方法定义的集合

  • interface可以被任意的对象实现

  • 一个对象可以实现任意多个interface

    type Human struct { name string age int phone string }

    type Student struct { Human //匿名字段Human school string loan float32 }

    type Employee struct { Human //匿名字段Human company string money float32 }

    //Human对象实现Sayhi方法 func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) }

    // Human对象实现Sing方法 func (h *Human) Sing(lyrics string) { fmt.Println("La la, la la la, la la la la la...", lyrics) }

    //Human对象实现Guzzle方法 func (h *Human) Guzzle(beerStein string) { fmt.Println("Guzzle Guzzle Guzzle...", beerStein) }

    // Employee重载Human的Sayhi方法 func (e *Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //此句可以分成多行 }

    //Student实现BorrowMoney方法 func (s *Student) BorrowMoney(amount float32) { s.loan += amount // (again and again and...) }

    //Employee实现SpendSalary方法 func (e *Employee) SpendSalary(amount float32) { e.money -= amount // More vodka please!!! Get me through the day! }

    // 定义interface type Men interface { SayHi() Sing(lyrics string) Guzzle(beerStein string) }

    type YoungChap interface { SayHi() Sing(song string) BorrowMoney(amount float32) }

    type ElderlyGent interface { SayHi() Sing(song string) SpendSalary(amount float32) }

11.2. interface值

  • 简单总结就是,只要是实现了接口的类型都可以赋值给接口

    package main

    import "fmt"

    type Human struct { name string age int phone string }

    type Student struct { Human //匿名字段 school string loan float32 }

    type Employee struct { Human //匿名字段 company string money float32 }

    //Human实现SayHi方法 func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) }

    //Human实现Sing方法 func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics) }

    //Employee重载Human的SayHi方法 func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) } // Interface Men被Human,Student和Employee实现 // 因为这三个类型都实现了这两个方法 type Men interface { SayHi() Sing(lyrics string) }

    func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} tom := Employee{Human{"Tom", 37, "222-444-XXX"}, "Things Ltd.", 5000} //定义Men类型的变量i var i Men //i能存储Student i = mike fmt.Println("This is Mike, a Student:") i.SayHi() i.Sing("November rain") //i也能存储Employee i = tom fmt.Println("This is tom, an Employee:") i.SayHi() i.Sing("Born to be wild") //定义了slice Men fmt.Println("Let's use a slice of Men and see what happens") x := make([]Men, 3) //这三个都是不同类型的元素,但是他们实现了interface同一个接口 x[0], x[1], x[2] = paul, sam, mike for _, value := range x{ value.SayHi() } }

11.3. 空interface

  • 空interface(interface{})不包含任何的method,正因为如此,所有的类型都实现了空interface。空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。

  • 一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数

  • 如果一个函数返回interface{},那么也就可以返回任意类型的值

    // 定义a为空接口 var a interface{} var i int = 5 s := "Hello world" // a可以存储任意类型的数值 a = i a = s

11.4. 接口类型判断

我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

11.4.1. Comma-ok断言

  • Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

  • 如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。

    package main

    import ( "fmt" "strconv" ) type Element interface{} type List [] Element type Person struct { name string age int } //定义了String方法,实现了fmt.Stringer func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 // an int list[1] = "Hello" // a string list[2] = Person{"Dennis", 70} for index, element := range list { if value, ok := element.(int); ok { fmt.Printf("list[%d] is an int and its value is %d\n", index, value) } else if value, ok := element.(string); ok { fmt.Printf("list[%d] is a string and its value is %s\n", index, value) } else if value, ok := element.(Person); ok { fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) } else { fmt.Printf("list[%d] is of a different type\n", index) } } }

11.4.2. switch测试

  • 这里有一点需要强调的是:element.(type)语法不能在switch外的任何逻辑里面使用,如果你要在switch外面判断一个类型就使用comma-ok

    package main

    import ( "fmt" "strconv" ) type Element interface{} type List [] Element type Person struct { name string age int } //打印 func (p Person) String() string { return "(name: " + p.name + " - age: "+strconv.Itoa(p.age)+ " years)" } func main() { list := make(List, 3) list[0] = 1 //an int list[1] = "Hello" //a string list[2] = Person{"Dennis", 70} for index, element := range list{ switch value := element.(type) { case int: fmt.Printf("list[%d] is an int and its value is %d\n", index, value) case string: fmt.Printf("list[%d] is a string and its value is %s\n", index, value) case Person: fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) default: fmt.Println("list[%d] is of a different type", index) } } }

11.5. 嵌入interface

  • Go里面真正吸引人的是它内置的逻辑语法,就像我们在学习Struct时学习的匿名字段,多么的优雅啊,那么相同的逻辑引入到interface里面,那不是更加完美了。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

    我们可以看到源码包container/heap里面有这样的一个定义

    type Interface interface { sort.Interface //嵌入字段sort.Interface Push(x interface{}) //a Push method to push elements into the heap Pop() interface{} //a Pop elements that pops elements from the heap }

    我们看到sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:

    type Interface interface { // Len is the number of elements in the collection. Len() int // Less returns whether the element with index i should sort // before the element with index j. Less(i, j int) bool // Swap swaps the elements with indexes i and j. Swap(i, j int) }

    另一个例子就是io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface:

    package main import ( "fmt" "os" )

    type Reader interface { Read(b []byte) (n int, err error) } type Writer interface { Write(b []byte) (n int, err error) } type ReadWriter interface { Reader Writer } func main() { var w Writer // os.Stdout 实现了 Writer w = os.Stdout fmt.Fprintf(w, "hello, writer\n") }

11.6. 接口反射

  • 使用reflect一般分成三步,下面简要的讲解一下:要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:

    t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素 v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

  • 转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如

    tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签 name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

  • 获取反射值能返回相应的类型和数值

    var x float64 = 3.4 v := reflect.ValueOf(x) fmt.Println("type:", v.Type()) fmt.Println("kind is float64:", v.Kind() == reflect.Float64) fmt.Println("value:", v.Float())

  • 最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误

    var x float64 = 3.4 v := reflect.ValueOf(x) v.SetFloat(7.1)

    如果要修改相应的值,必须这样写

    var x float64 = 3.4 p := reflect.ValueOf(&x) v := p.Elem() v.SetFloat(7.1)

    因为传值,传的是拷贝的副本,如果想改变就必须改变原始值

11.7. 隐式接口

  • 类型通过实现那些方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implements“。
  • 隐式接口解藕了实现接口的包和定义接口的包:互不依赖。(也就是说接口可以跨包实现)

11.8. 接口的几个实际应用

11.8.1. Stringers

  • 一个普遍存在的接口是 fmt 包中定义的 Stringer

    type Stringer struct { String() string }

    package main import ( "fmt" "strconv" )

    type Human struct { name string age int phone string }

    // 通过这个方法 Human 实现了 fmt.Stringer func (h Human) String() string { return "❰"+h.name+" - "+strconv.Itoa(h.age)+" years - ✆ " +h.phone+"❱" }

    func main() { Bob := Human{"Bob", 39, "000-7777-XXX"} fmt.Println("This Human is : ", Bob) }

  • 这也是实现了fmt.Stringer这个interface,即如果需要某个类型能被fmt包以特殊的格式输出,你就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。

  • 实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了

11.8.2. 错误error

  • 这个地方有一个内建接口的概念,Go 程序使用 error 值来表示错误状态。

    type error interface { Error() string }

    (与 fmt.Stringer 类似,fmt 包在输出时也会试图匹配 error。)

    通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil, 来进行错误处理。

    i, err := strconv.Atoi("42") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) } fmt.Println("Converted integer:", i)

    error 为 nil 时表示成功;非 nil 的 error 表示错误。

    package main
    
    import (
        "fmt"
        "time"
    )
    
    type MyError struct {
        When time.Time
        What string
    }
    
    func (e *MyError) Error() string {
        return fmt.Sprintf("at %v, %s",
            e.When, e.What)
    }
    
    func run() error {
        return &MyError{
            time.Now(),
            "it didn't work",
        }
    }
    
    func main() {
        if err := run(); err != nil {
            fmt.Println(err)
        }
    }
    

12. 并发

12.1. goroutine

  • goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。

  • goroutine是通过Go的runtime管理的一个线程管理器。

  • goroutine通过go关键字实现了,其实就是一个普通的函数。

    go f(x, y, z)

  • goroutine 在相同的地址空间中运行,因此访问共享内存必须进行同步。sync 提供了这种可能,不过在 Go 中并不经常用到,因为有其他的办法

  • 不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。

12.2. channel(信道)

  • goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值。

    ci := make(chan int) cs := make(chan string) cf := make(chan interface{})

  • channel通过操作符<-来接收和发送数据

    ch <- v // 发送v到channel ch. v := <-ch // 从ch中接收数据,并赋值给v

12.2.1. 无缓冲信道

  • 无缓冲的信道永远不会存储数据,只负责数据的流通,为什么这么讲呢?

    • 从无缓冲信道取数据,必须要有数据流进来才可以,否则当前线阻塞
    • 数据流入无缓冲信道, 如果没有其他goroutine来拿走这个数据,那么当前线阻塞
  • 默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。☆☆☆☆☆无缓冲channel是在多个goroutine之间同步很棒的工具☆☆☆☆☆

    package main

    import "fmt"

    func sum(a []int, c chan int) { total := 0 for _, v := range a { total += v } c <- total // send total to c }

    func main() { a := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x + y) }

  • 由上可知信道有阻塞主线程的作用,也就是无缓冲的信道在取消息和存消息的时候都会挂起当前的goroutine,除非另一端已经准备好

    var ch chan int = make(chan int)

    func foo() { ch <- 0 // 向ch中加数据,如果没有其他goroutine来取走这个数据,那么挂起foo, 直到main函数把0这个数据拿走 }

    func main() { go foo() <- ch // 从ch取数据,如果ch中还没放数据,那就挂起main线,直到foo函数中放数据为止 }

12.2.2. 死锁

  • 总结来看,为什么会死锁?非缓冲信道上如果发生了流入无流出,或者流出无流入,也就导致了死锁。或者这样理解 Go启动的所有goroutine里的非缓冲信道一定要一个线里存数据,一个线里取数据,要成对才行

    死锁1 func main() { ch := make(chan int) <- ch // 阻塞main goroutine, 信道c被锁 }

    死锁2 var ch1 chan int = make(chan int) var ch2 chan int = make(chan int)

    func say(s string) { fmt.Println(s) ch1 <- <- ch2 // ch1 等待 ch2流出的数据 }

    func main() { go say("hello") <- ch1 // 堵塞主线 }

12.3. Buffered Channels(缓冲 channel)

  • Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。

    ch := make(chan type, value) 当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入

    package main

    import "fmt"

    func main() { c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 c <- 1 c <- 2 fmt.Println(<-c) fmt.Println(<-c) } //修改为1报如下的错误: //fatal error: all goroutines are asleep - deadlock!

12.4. range和close

  • 上面这个例子中,我们需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel

    package main

    import ( "fmt" )

    func fibonacci(n int, c chan int) { x, y := 1, 1 for i := 0; i < n; i++ { c <- x x, y = y, x + y } close(c) }

    func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } }

    for i := range c能够不断的读取channel里面的数据,直到该channel被显式的关闭(这句话说明这个循环会一直存在,直到你调用了close)。上面代码我们看到可以显式的关闭channel,生产者通过内置函数close关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法v, ok := <-ch测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。

  • 记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

  • 另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的

12.5. select

  • 我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

  • select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

    • (也就是用了select之后就一直处于监听状态了,只要有管道的读写操作就会执行select,因为select被包裹在for循环中了)

    package main

    import "fmt"

    func fibonacci(c, quit chan int) { x, y := 1, 1 for { select { case c <- x: x, y = y, x + y case <-quit: fmt.Println("quit") return } } }

    func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }

  • 在select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)

    select { case i := <-c: // use i default: // 当c阻塞的时候执行这里 }

12.6. 超时

  • 有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现:

    func main() { c := make(chan int) o := make(chan bool) go func() { for { select { case v := <- c: println(v) case <- time.After(5 * time.Second): println("timeout") o <- true break } } }() <- o }

12.7. runtime goroutine

runtime包中有几个处理goroutine的函数:

  • Goexit:退出当前执行的goroutine,但是defer函数还会继续调用
  • Gosched:让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
  • NumCPU:返回 CPU 核数量
  • NumGoroutine:返回正在执行和排队的任务总数
  • GOMAXPROCS:用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

13. 免责说明

  • 本文档中的部分内容摘自网上的众多博客,仅作为自己知识的补充和整理,并分享给其他需要的coder,不会用于商用。
  • 因为很多博客的地址看完没有及时做保存,所以很多不会在这里标明出处,非常感谢各位大牛的分享,也希望大家理解。
  • 如果原文作者感觉不适,可以及时联系我shiguoqing999@163.com,我将及时删除争议部分内容

14. 追责声明

  • 如有大段引用超过全文50%的内容,请在文档结尾标明原文出处:龙马行空-石国庆-朱庇特-https://my.oschina.net/u/1416844/blog,否则将视为抄袭,予以法律追究,请各位尊重个人知识产权。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这