我要偷偷的学Go语言,然后惊呆所有人(第二天)

标题无意冒犯,就是觉得这个广告挺好玩的
上面这张思维导图喜欢就拿走

目录

Go语言的包结构

在我们的每一段程序的开头, 都要使用package关键字来声明这段代码属于哪个包, 但是关于Go语言的包管理机制我们还没有探讨过, 因此在这一节我们讲一下关于Go语言的包(package):

3.1 包引用

Go语言的代码一般位于$GOPATH/src下, Go语言的标准包可以直接引用, 第三方的包和自己定义的包必须放在这个目录下才可以引用.

比如: 如果我们要使用Go语言官方的strings包中的函数, 那么我们需要在代码中这样去引用:

import (
    "strings"
)

但是比如我们要引用Go语言中比较好用的ORM包: gorm的时候, 作为一个第三方的包, 我们应该这样去引用:

import (
    "github.com/jinzhu/gorm"
)

当然, 这都是使用绝对路径的引用方式, 我们还可以使用相对路径的引用方式: 使用./, ../这种方式去进行包的引用.

在引用之后, 我们就可以使用包名.的方式去使用包中的方法和变量了, 比如使用Go语言中的sort包的Ints方法:

sort.Ints(integerSlice)

但是有的时候我们引用的包名会有冲突的状况, 又或者是这个包的名字太长, 写起来不太方便, 总之各种原因让我们需要给他一个别名, 那么这个时候就应该使用这样的方式去进行引用:

import (
    S "sort" // import nickName "packagePath"
)

// ...
S.Ints(integerSlice)
// ...

或者可以直接将包中的内容和当前代码做一个合并, 让我们可以直接调用:

import (
    . "sort"
)

// ...
Ints(integerSlice)
// ...

如果我们只是想调用一个包的初始化函数, 而并不会使用其中的任何一个其他的函数或者变量, 我们应该怎么办呢? 相信大家在建立数据库连接时都是这样去做的, 为了应对这样的情况, 在Go语言下可以使用下划线_来导入一个包:

import (
    _ "packagePath" // 仅执行初始化函数 init()
)
3.2 包加载

我们现在知道了该如何对包进行引用, 也就是对于import关键字的用法. 那么Go语言是以一种怎样的顺序去加载包的呢? 在这一节就来讲一下这个问题:

在这里插入图片描述

所以我先画了一个图在这里, 从这里大家可以看到包加载的基本过程:

  • 在加载时, 如果有引入其他的包, 那么进入该包的加载过程
  • 如果对于引入的包的加载完毕了, 那么开始进行常量的声明
  • 常量的声明后是变量的声明
  • 如果该包有init()函数, 那么执行init()函数
  • 如果该包是main包, 那么在init()之后执行main()函数

其实加载的主要思想很简单, 就是记住加载的顺序就好了.

3.3 封装

封装是面向对象编程思想中很重要的一部分, 封装的具体含义指隐藏具体的实现细节, 仅暴露出可以调用的接口和方法.

具体关于封装的细节就不详谈了, 因为一般教科书或者网络上关于封装的描述都是基于的, 而Go语言的面向对象的实现不太一样, 因此不再赘述, 这个小节只关心如何实现封装, 也就是如何实现字段和方法对外的隐藏.

在Go语言中区分是否暴露给其他包的标志是名称是否以大写字母开头, 如果是以大写字母开头的类型, 变量, 常量或者函数, 是可以在当该包被其他包导入的时被暴露的. 反之则只能够在该包中进行使用.

同样的, 一个类型上绑定的方法, 如果是以大写字母开头, 则是可以被调用的. 类型中的字段也是一样的.

最后, Go语言中有大量的内置的包和第三方开发的包, 请大家不要重复造轮子, 灵活使用别人的轮子会事半功倍.


变量和数据类型

一般来说,强类型语言中的变量是盒子,而弱类型语言中的变量则多数是标签

因为强类型,所以一个变量能够分配的内存空间相对来说是便于计算的,因此语言内部会在内存中为你的变量开辟一块内存空间,而这个内存空间的地址会被映射为变量名,而内存空间内存放的数据是什么并不被关心,就像是一个盒子,里面放了不同的数据。

4.1 变量的声明:

在Go语言中,声明一个新变量的方式有两种:var关键字和:=运算符。

1. 使用var关键字

var hello string // 声明一个变量hello,类型为string,默认值为string的零值,也就是空字符串。
var hello string = "Hello, golang!"
var hello, world string = "Hello", "World" // 可以同时声明多个变量并且初始化

var hello = "Hello, golang!" // 根据右侧值的类型来推断变量类型

2. 使用:=运算符
:=运算符用于简化新变量声明的步骤,不希望程序员每次用到新变量都去使用var来声明。因此:=运算符只能用于新变量的声明

hello := "Hello, golang!" // 等价为 var hello = "Hello, golang",也会进行类型的推断

var hello string; hello := "Hello, golang!" // 会报错,即使声明的变量未曾使用过,也不是一个新变量,因此不能使用 := 运算符

hello, err := someFunc() // 便捷地接收函数返回的结果,前提也是一定是新的变量名
4.2 Go语言的数据类型:

Go语言的类型系统非常的简单多变,不同的组合形式可以产生成百上千上万的类型,也就是说,通过不同方式的组合,Go语言中可以有无数个类型

1. 基本的数据类型

类型名称标识符描述
布尔类型bool布尔值仅有truefalse两种值
字符串类型string在Go语言中的字符串是不可变的常量
字节类型byte字节类型,实质上是uint8
Unicode字符类型rune实质上是int32类型
有符号整数类型int, int8, int16, int32, int64int是基于具体架构的类型,后面的数字代表在内存中所占的位数
无符号整数类型uint, uint8, uint16, uint32, uint64uint是基于具体架构的类型,后面的数字代表在内存中所占的位数
浮点数类型float32, float64, complex64, complex128float32float64都是遵循IEEE-754标准,数字依然表示位数,而complex64complex128分别表示32位和64位的实数和虚数

2. 派生类型

2.1 指针类型(pointer)

Go语言是内存安全的,但是开放了灵活的指针给程序员使用,又不涉及复杂的指针运算。

在Go语言中的类型都有对应的指针类型,以*为标识符,例如:

var stringPointer *string
var intPointer *int

指针类型的变量存储着内存地址。可以使用&取地址符来获取一个变量的地址。

Go语言中的空指针为nil

2.2 数组类型(array)

Go语言中的任意类型都可以有相应确定长度的数组类型:

var fourStringArray [4]string // 一个长度为4的字符串数组
var int2dArray [4][5]int // 一个长度为4的 长度为5的int数组 的数组,也就是一个二维数组

值得注意的是:这里的数组名不代表数组的开始地址!,需要使用&来取得首地址,也不建议对它进行指针运算。

定长的原因很简单:可以很好的确定内存空间的大小。

2.3 结构化类型(struct)

结构体,没有人会陌生,如果这不是你接触的第一门语言的话。

结构化的自定义类型被广泛应用于组织和传递数据,以及Go语言的面向对象也是以结构体作为支撑的。
例子:

var myName struct {
    FirstName  string // 字段名在前,类型名称在后
    LastName   string
} // 声明了一个结构体变量

// 定义一个新的结构体类型

type Name struct {
    FirstName  string
    LastName   string
}

// 如何初始化一个结构体变量
myName := Name {FirstName: "Shiina", LastName: "Mashiro"} // 最后不需要添加逗号

myName := Name {
    FirstName: "Shiina",
    LastName:  "Mashiro, // 需要添加逗号
}

// 使用某个字段
lastName := myName.LastName

也可以用组合的方式定义新的结构体:

type Info struct {
    Tel     string
    Address string
}

type Detail struct {
    Name
    Info
}

Go语言中没有类 (class),但是会给相应的类型绑定方法,具体的形式会在后面面向对象的部分进行讲解。

2.4 通道类型(channel)

如果你学习过CSP,或者你是仰慕Go语言的并发语义和并发模式而来,那么你一定知道什么是通道(channel):通道是一个可以带有一定缓冲的队列,用于goroutine之间的通信。

通过一个例子来看一下如何使用(channel可是Go语言的特色之一啊,睁大眼睛):

func tryChannel() {
    ch := make(chan int, 10)       // 使用make函数构造一个缓冲容量为10的int通道
    for i:=0; i<10; i++ { ch<- i } // 循环的向通道中塞十个数据
    for value := range ch {        // 遍历通道(循环的取出数据)
        fmt.Println(value) 
        if value == 9 { break }    // 这里如果不结束循环的话,会引发DeakLock,具体的原因会在后面讲解,现在只是为了介绍类型
    }
    close(ch)                      // 手动关闭通道
}

如果在调用make()方法的时候,没有传入通道的缓冲容量,则默认为0,也就是说同时传入和传出,否则的话只会阻塞。如果程序中的所有goroutine都阻塞了,则会触发DeadLock,这也解释了例子中为什么要中断循环。

channel是具有队列的性质的,也就是FIFO, First in First out,因此把channel当做队列数据结构来用的情况也是很多的(但是我更建议去手动实现和管理)。

2.5 函数类型(function)

在越来越多的支持面向对象编程语言中,函数被当做一等公民看待。因此也就有专门的函数对象,变量,类型。

函数变量的类型由几个因素组成:函数名参数返回值

例子:

type compareFunction func(int, int) int
var min compareFunction = func(x, y int) int {
    if x < y {
        return x
    }
    return y
}

fmt.Println(compareFunction(10, 20))

同时函数当然也可以作为参数传入函数。

func less(x, y int, comp func(int, int) bool) {
    if comp(x, y) {
        return x
    }
    return y
}

func tryLess() {
    comp := func(x, y int) bool { return x < y }
    fmt.Println(less(10, 20, comp))
}

匿名函数的使用等等,会在后面进行介绍。

2.6 切片类型(slice)

人们总是抱怨数组的定长不够方便,而在C++ STLvector出现后,大家就开始疯狂使用这个名为动态数组的东西,还可以自己开辟内存空间,这真是太好用了!

动态数组出现之后,人们又在其基础之上发明了切片(slice),不仅长度可变,而且有很多利用[start:end:step]进行的操作。最典型的便是Python中的列表

但是这是Go语言,所以我们来看看Go语言的切片:

type intSlice []int  // 在类型定义上,和数组除了长度没有区别

func trySlice() {
    ints := intSlice{1, 2, 3, 4, 5}
    fmt.Println(len(ints), ints[1:3])// 使用len方法获取切片的长度,也可以使用cap方法获取切片的容量
    
    anotherInts := make(intSlice, 5) // 使用make方法创建一个长度为5的切片
    copy(anotherInts, ints)          // 使用copy方法复制内容到新切片
    fmt.Println(append(ints, 6))     // 使用append方法增加新元素到切片,但是不是原地操作
    fmt.Println(append(ints, anotherInts...))
}

2.7 接口类型(interface)

如果说能让程序员在这个强类型强到天上的语言中找到什么慰藉的,大概就是强大的接口了,至于接口是什么,可以看这个

定义接口的方式和定义结构体的方式相似:

type HumanNature interface {
    Speak() string   // 接口匹配方法
    See(interface{}) // 空接口默认可以匹配所有的类型
}

接口的知识太多了,放在以后赘述。

2.8 映射类型(map)

映射,很方便,很实用。因为查询的效率,无序性等等。

map的派生类型是由键类型值类型共同决定的。常见的构建方式是使用make()方法。

func tryMap() {
    newMap := make(map[int]string)
    newMap[1] = "C"           // 插入键值对
    
    if value, have := newMap[2]; have { // 通常用这种方式检查是否有对应的键值对
        // ...some code here
    } else { fmt.Println("Key: 2 Not Found!") }
    newMap[2] = "C++"; newMap[3] = "Java"
    
    for key, value := range newMap { // 通过这种形式遍历map
        fmt.Println(key, "-", value)
    }
    delete(newMap, 1)                // 通过内置的delete方法来删除键值对
}

map是使用Go语言时非常常用的类型,不论是编写并发代码还是正常代码,都是非常重要的。

扩展阅读:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小夕Coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值