A Bite of GoLang (2)

叁叁肆2018-09-19 11:04


本文来自网易云社区


作者:盛国存


2.3、Map

1、创建map

var m1 map[string]int
m2 := make(map[string]int)

上述就是常见的创建map的方式,但是m1、m2还是有区别的,m1是nil,m2是一个空map;常规的遍历map也是用 range 的方式就可以,

for k, v := range m {
    fmt.Println(k, v)
}

当然细心的会发现,在遍历的过程中是不能保证顺序的,当然要是想顺序遍历,需要自己手动对key进行排序,可以将key存进slice,然后再通过slice遍历相关的key获取map的值。

2、获取map元素

m[key] 一般就是这样获取map的值

var map1 = map[string]string{

    "name" : "shengguocun",
    "gender" : "male",
    "city" : "hangzhou",
}

value1 := map1["age"]
fmt.Println(value1)

先来猜测一下,上述这段代码可以运行么?会不会报错?

答案是不会,这就是Go语言和别的语言不一样的地方,上述的例子中 value1 的值是一个空字符串,map中当key不存在时,会获取value类型的初始值。

gender, ok := map1["gender"]

if ok {
    fmt.Println("Gender 的值为 : ", gender)
}else {
    fmt.Println("Key 不存在")
}

既然Go语言的出现就是为了解决别的语言的痛点,所以在使用过程中不再需要每次获取某个 key 的时候都要去 isset 判断一下,Go的获取map的值的时候第二个返回值就是别的语言 isset 的功能;存在返回 true ,不存在返回 false。

3、删除元素

delete函数,就可以直接删除指定的key的值

这是Go语言的官方文档,不难理解比如要删除上面的 map1 的 city 的值

delete(map1, "city")

直接调用就可以

4、map的key

为什么要把key单独拿出来说呢?因为map底层使用的是hash表,所以map的key必须可以比较相等;换句话说就是除了 slice、map、function的内建类型都可以作为key。


2.4、字符和字符串处理

1、rune介绍

rune就是Go语言的字符串类型,其实可以理解为是 int32 的一个別名,下面我们通过例子来深入理解一下rune

s1 := "你好,杭州"
fmt.Println(s1)

for _, ch := range []byte(s1) {

    fmt.Printf("%X ", ch)
}

fmt.Println()

for i, ch := range s1 {

    fmt.Printf("(%d %X) ", i, ch)
}

输出结果

你好,杭州
E4 BD A0 E5 A5 BD 2C E6 9D AD E5 B7 9E
(0 4F60) (3 597D) (6 2C) (7 676D) (10 5DDE)

从上述的例子我们可以直接的看出来,其实就是将UTF-8编码解码,然后再转成Unicode之后将它存放进一个rune(int32)中

2、字符串处理

UTF-8编码的rune长度统计

count := utf8.RuneCountInString(s1)
fmt.Println("Rune Count :", count)

输出结果为:

Rune Count : 5

字符串的输出操作

bytes := []byte(s1)
for len(bytes) > 0 {
    ch, size := utf8.DecodeRune(bytes)
    bytes = bytes[size:]
    fmt.Printf("%c ", ch)
}

用rune实现上述同样的功能

for _, ch := range []rune(s1) {
    fmt.Printf("%c ", ch)
}


3. 面向“对象”

3.0、结构体和方法

1、结构体的创建

go语言仅支持封装,不支持继承和多态;这句话怎么理解呢?就是说在Go语言内部没有class,只有struct;也没有复杂的继承和多态,那继承和多态的任务又是通过什么实现的呢?Go是面向接口编程,可以通过接口来实现继承和多态的相关的任务,后面我会再进行介绍。 下面先来介绍一下struct的创建:

type Node struct {
    Value       int
    Left, Right *Node
}

通过type、struct关键字创建结构体类型,当然在创建了结构体类型之后,就可以创建相关类型的变量

var root tree.Node
root = tree.Node{Value:1}
root.Value = 2
root.Left = &tree.Node{Value:3}
root.Right = &tree.Node{}

2、方法创建

结构体的方法的创建和普通的函数创建没有太大的区别,只是在方法名前面添加一个接收者,就相当于其他语言的this

func (node Node) Print() {
    fmt.Print(node.Value, " ")
}

上述就是一个值接收者打印出Node的Value的值的方法。 当然要是需要改变Value的值的时候,就需要一个指针接收者。

func (node *Node) SetValue(value int) {
    node.Value = value
}

有一个疑问,要是对一个值为nil的Node进行 SetValue 操作会发生什么?

var pRoot *tree.Node
pRoot.SetValue(1)

虽说nil指针可以调用方法,但是下面的Value是拿不到,自然就会报下面的错了

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x20c3]

goroutine 1 [running]:
panic(0x8f100, 0xc42000a070)

实际使用过程中可以添加相关的判断在做处理。结合上面的知识我们不难写出一个树的遍历的方法的代码

func (node *Node) Traverse()  {

    if node == nil {
        return
    }
    node.Print()
    node.Left.Traverse()
    node.Right.Traverse()
}


3.1、包和封装

1、命名规范

  • 名字一般使用 CamelCase(驼峰式)
  • 首字母大写:Public
  • 首字母小写:Private

2、包的概念

  • 每个目录一个包,但是包名和目录名不一定要一样的,但是每个目录只能包含一个包;
  • main包是一个相对特殊的,main包包含一个可执行入口;
  • 为结构体定义的方法必须放在同一个包内

当然上面的例子已经在不经意间提前引入了package的概念


3.2、扩展已有类型

在面向对象中,我们想要扩展一下别人的类,我们通常继承一下就好了,但是Go语言中没有继承的概念,我们该如何处理呢?

1、定义别名(1.9新特性)

在大规模的重构项目代码的时候,尤其是将一个类型从一个包移动到另一个包中的时候,有些代码使用新包中的类型,有些代码使用旧包中的类型

基本语法就是:

type identifier = Type

比如内建的byte类型,其实是uint8的类型别名,而rune其实是int32的类型别名。

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

通过别名的方式就可以拓展了,比如

type T1 struct{}
type T3 = T1
func (t1 T1) say(){}
func (t3 *T3) greeting(){}
func main() {
    var t1 T1
    // var t2 T2
    var t3 T3
    t1.say()
    t1.greeting()
    t3.say()
    t3.greeting()
}

当然要是T1也定义了 greeting 的方法,那么编译会报错的,因为有重复的方法定义。

2、使用组合

比如我们想扩展上面的树的包,实现一个自己的中序遍历,该如何实现呢?通过代码来理解一下使用组合的概念

type myNode struct {
    node *tree.Node
}

func (myNodeNode *myNode) Traverse() {

    if myNodeNode == nil || myNodeNode.node == nil {
        return
    }
    left := myNode{myNodeNode.node.Left}
    right := myNode{myNodeNode.node.Right}
    left.ownFunc()
    myNodeNode.node.Print()
    right.ownFunc()
}


3.3、GOPATH以及目录结构

  • 默认在 ~/go 目录下(unix或者Linux环境),%USERPROFILE%\go 目录下(windows环境)
  • 官方推荐:所有的项目和第三方库都放在同一个GOPATH下
  • 当然也可以将每个项目放在不同的GOPATH下

如何查看自己的GOPATH呢?

~ sheng$ echo $GOPATH
/Users/verton/GoLangProject

1、go get获取第三方库

go get url

这样是可以获取GitHub上面的三方的库,但是Golang.org上面要是不能翻墙是获取不了的,这里我给大家介绍一个新的工具 gopm

sheng$ go get github.com/gpmgo/gopm

一行命令就可以装好了,这个时候再get三方的库就毫无压力了,因为这个国内有相关的镜像

gopm get -g url

采用-g 参数,可以把依赖包下载到GOPATH目录中

2、目录结构

  • src
    • git repo 1
    • git repo 2
  • pkg
    • git repo 1
    • git repo 2
  • bin
    • 执行文件 1 2

从上述的目录结构上我们可以看出来,src pkg 是对应的,src 是我们的代码的位置以及三方库的位置,pkg 是build的中间过程,可以暂时先不用关注,bin下面就是可执行文件。


4. 面向接口

4.0、Duck Typing的概念

很多语言都有duck typing的概念, 用一个简单的例子来描述一下这个概念

大黄鸭是鸭子么?这个答案是要看基于什么角度来看,从生物角度来看,那它当然不是鸭子,连基本的生命都没有;但是从duck typing的角度来看它就是一个鸭子,因为它外部长得像鸭子,通俗点概括一下duck typing的概念就是:描述事物的外部行为而非内部结构。

从严格意义上讲,go语言只能说是类似duck typing,go语言不是动态绑定的,go语言是编译时绑定的。


4.1、接口的定义和实现

在Go语言中,接口interface其实和其他语言的接口意思也没什么区别。一个结构体必须实现了一个接口的所有方法,才能被一个接口对象接受,这一点和Java语言中的接口的要求是一样的。interface理解其为一种类型的规范或者约定。

1、接口的定义

type Retriever interface{
    Get(url string) string
}

这样就定义了一个接口,它包含一个Get函数。

2、接口的实现

现在我们就来实现一下这个接口。比如我们做一个拉取某个页面的操作

package rick

import (
    "net/http"
    "net/http/httputil"
)

type Retriever struct {

}

func (r Retriever) Get(url string) string {

    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }
    result, err := httputil.DumpResponse(resp, true)
    resp.Body.Close()
    if err != nil {
        panic(err)
    }
    return  string(result)
}
package main

import (
    "shengguocun.com/retriever/rick"
    "fmt"
)

type Retriever interface{
    Get(url string) string
}

func download(r Retriever) string {
    return r.Get("http://www.shengguocun.com")
}

func main() {

    var r Retriever
    r = rick.Retriever{}
    fmt.Println(download(r))
}

上述rick.Retriever就实现了Retriever接口。


4.2、接口值的类型

1、接口变量里面有什么

继续使用上面的例子

var r Retriever
r = rick.Retriever{}

fmt.Printf("\n %T %v \n", r, r)

会输出什么呢?

rick.Retriever {}

这就是常规的值传递,没有什么特别的地方。要是 Retriever 这个struct很大,我们不希望通过传值的方法去拷贝,而是通过指针访问Get方法。

func (r *Retriever) Get(url string) string {

    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }
    result, err := httputil.DumpResponse(resp, true)
    resp.Body.Close()
    if err != nil {
        panic(err)
    }
    return  string(result)
}
var r Retriever
r = &rick.Retriever{}

fmt.Printf("\n %T %v \n", r, r)

这时候的Type、Value又是什么?

*rick.Retriever &{}

我们可以看到是一个指针,所以我们一般用到接口的指针,因为它的肚子里含有一个指针,通常我们会说“接口变量自带指针”,那我们现在用两个图来总结一下上面的概念

概括为:接口变量里面可以是实现者的类型和实现者的值,或者是接口类型里面可以是实现者的类型和实现者的指针,同时指向实现者。

2、查看接口变量

说到这里要提到一个特殊的接口,空接口 interface{} ,对于空接口 interface{} 其实和泛型的概念很像,任何类型都实现了空接口。在方法需要返回多个类型的时候,返回值的类型我们一般定义为 interface{} 。

这时我们现在引入获取接口变量肚子里的类型的另外一种写法,叫 Type Assertion(断言)。比如

var a interface{}
fmt.Println("Are you ok?", a.(string))

然而上述的写法一旦断言失败,会报出panic错误,当然这样的程序就显得十分的不友好。我们需要在断言前进行一个判断。

value, ok := a.(string)
if !ok {
    fmt.Println("断言失败,这不是一个string类型")
    return
}
fmt.Println("值为:", value)

另外我们可以结合switch进行类型判断

var r interface{}
r = balabalaFunction()
switch v := r.(type) {
    case bool:
        fmt.Println("type bool...")
    case int:
        fmt.Println("type int...")
}

Tips:转换类型的时候如果是string可以不用断言,使用fmt.Sprint()函数可以达到想要的效果。


相关阅读:A Bite of GoLang (1)

A Bite of GoLang (2)

A Bite of GoLang (3)

A Bite of GoLang (4)

A Bite of GoLang (5)

A Bite of GoLang (6)

A Bite of GoLang (7)

A Bite of GoLang (8)


网易云免费体验馆0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区