前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >A Bite of GoLang(上)

A Bite of GoLang(上)

原创
作者头像
盛国存
修改2018-05-14 18:07:42
1.1K4
修改2018-05-14 18:07:42
举报
文章被收录于专栏:盛国存的专栏盛国存的专栏

0. 前言

A bite of GoLang(浅尝GoLang),本文只是Go语言的冰山一角,本文包含作者学习Go语言期间积累的一些小的经验,同时为了方便让读者了解到Go语言中的一些概念,文中包含了许多快速简洁的例子,读者后期可以去自行拓展。当然写这篇文章的灵感来源于GitHub上的 a bite of Python

1. 基础

1.0、环境搭建

1、下载安装包安装

通过浏览器访问下面的地址 https://golang.org/dl/ 要是自己的网络不能访问外国网站的话,可以访问下面的Go语言中文网 https://studygolang.com/dl 下载指定的版本的安装包直接下一步就可以安装完成;

2、命令行安装

Mac 利器 home brew 安装 go

代码语言:javascript
复制
brew update && brew upgrade
brew install git
brew install mercurial
brew install go

安装完成之后

代码语言:javascript
复制
vim ~/.bashrc

代码语言:javascript
复制
#GOROOT
export GOROOT=/usr/local/Cellar/go/1.7.4/libexec

#GOPATH
export GOPATH=$HOME/GoLangProject

#GOPATH bin
export PATH=$PATH:$GOPATH/bin

#GOPATH root bin
export PATH=$PATH:$GOROOT/bin

代码语言:javascript
复制
source ~/.bashrc

OK配合完成之后,输入go env验证一下是否配置成功

代码语言:javascript
复制
~ sheng$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/verton/GoLangProject"
GORACE=""
GOROOT="/usr/local/Cellar/go/1.7.4/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.7.4/libexec/pkg/tool/darwin_amd64"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/z2/h48yrw8131g824_bvtw6584r0000gn/T/go-build415367881=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"

1.1、变量定义

1、通过var关键字

代码语言:javascript
复制
var a int
var b string

在Go语言中在定义变量的时候,是变量在前类型在后,现在你暂时先不用考虑那么多为什么,就先知道Go是这样的定义形式就可以了;当然可以多个变量一起定义,同时可以一起赋初值

代码语言:javascript
复制
var a,b,c bool
var m,n string = "Hello","World"
var (
    aa = 1
    bb = "hello world"
    cc = true
)

当然也可以让编译器自动决定类型,比如

代码语言:javascript
复制
var s,m,p,q = 1,"hahah",false,"xixiix"

2、使用 := 定义变量

代码语言:javascript
复制
s,m,p,q := 1,"hahah",false,"xixiix"

这样呢可以让代码写的更加简短一点,当然呢 := 只能在函数内使用,是不能在函数外使用的。(相关的函数的知识后面会做介绍)

1.2、内建变量类型

1、bool 、string

这两个类型就不做过多的介绍,因为基本每一门语言里面都有这两个类型,在Go语言里面也是一样的

2、(u)int、(u)int8、(u)int16、(u)int32、(u)int64、uintptr

上面这些就是Go的整数类型,加u和不加u的区别就是有无符号的区别,Go语言中的整数类型还分为两个大类,一个是规定长度的,比如:int8、int16、int32...,还有一种就是不规定长度的,它是根据操作系统来,在32位系统就是32位,在64位系统就是64位的,Go语言中没有int、long 这些类型,你想要定义一个相对较长的定义int64就可以了,最后uintptr就是Go语言的指针,后面我会再来介绍它

3、byte、rune

byte就不用过多介绍了,大家都知道字节类型,那rune是什么呢,这就是Go语言的“char”,因为char只有一个字节在使用中会有很多的坑,Go语言针对这点痛点做了一些优化

4、float32、float64、complex64、complex128

前面两个不过多介绍,浮点数类型32位和64位的,后面两个是一个复数的类型,complex64实部和虚部都是32位的,complex128实部和虚部都是64位的

1.3、常量与枚举

代码语言:javascript
复制
const a = 1
const b,c = 2,3
const (
    d = 5
    e,f = 6,7
)

常量数值可以作为各种类型使用,比如以下代码

代码语言:javascript
复制
var s,p = 3,4
m := math.Sqrt(s*s + p*p)
fmt.Println(m)

这段代码语法是编译不通过的,因为Sqrt的参数必须是一个浮点数类型;但是呢我们把是s、p定义成常量就可以编译通过了

代码语言:javascript
复制
const s,p = 3,4
m := math.Sqrt(s*s + p*p)
fmt.Println(m)

Go语言中的枚举类型就是通过const来实现,同时Go语言中还可以通过iota实现自增的功能

代码语言:javascript
复制
func enums(){

	const (
		a = iota
		b
		c
	)
	fmt.Println(a, b, c)
}

调用上面这个函数显而易见,会输出

代码语言:javascript
复制
0 1 2

1.4、条件语句

1、if

正常的条件判断我这边就不做过多的介绍,当然Go语言有它特别的地方,if的条件里可以赋值,比如:

举个读文件的例子,ioutil.ReadFile 这个方法有两个返回值,后面会详细的讲解,常规的写法是

代码语言:javascript
复制
const filename  = "file.txt"
content,err := ioutil.ReadFile(filename)
if err != nil {
	fmt.Println(err)
}else {
	fmt.Println(string(content))
}

Go语言可以整合成下面的写法

代码语言:javascript
复制
const filename  = "file.txt"
if content,err := ioutil.ReadFile(filename); err != nil {
	fmt.Println(err)
}else {
	fmt.Println(string(content))
}

2、switch

代码语言:javascript
复制
func eval(a int, b int, op string) int {

	var result int
	switch op {

		case "+":
			result = a + b
		case "-":
			result = a - b
		case "*":
			result = a * b
		case "/":
			result = a / b
		default:
			panic("unsupported op")
	}
	return result
}

看上面的这段代码,你发现和别的语言不一样的地方是怎么没有break,是的,Go语言中switch会自动break,除非使用fallthrough

同时,Go语言的switch还有另外一种写法,结合一个最常见的Switch用法举个例子吧,比如通过考试分数判断是否合格

代码语言:javascript
复制
func grade(score int) string {

	switch {
		case score > 100 || score < 0:
			panic("Wrong score")
		case score > 80:
			return "A"
		case score > 70:
			return "B"
		case score > 60:
			return "C"
		default:
			return "D"
	}
}

上面的一个写法可以发现switch后面是可以没有表达式的

1.5、循环

1、for

for关键字和其他语言有着共同的功能,同时还充当的Go语言中的 while 功能,Go语言中没有 while 关键字

代码语言:javascript
复制
for scanner.Scan() {
	fmt.Println(scanner.Text())
}

上面的循环代码省略了起始条件,省略了递增条件,就跟while的功能非常的类似

代码语言:javascript
复制
for {
    fmt.Println("hello world")
}

上面其实就是一个死循环,因为Go语言中经常会用到,后面的并发编程 Goroutine 的时候还会给大家继续介绍。

1.6、函数

1、普通函数

普通的函数定义我这边不再过多阐述,跟变量定义类似,函数名在前,函数返回类型在后

2、多返回值

这个是Go语言的不一样的地方,函数可以有多个返回值,比如 ioutil.ReadFile 这个函数就是有两个返回值,但是呢多返回值不要滥用,尽量贴合Go语言的风格,常规返回值和一个error,那我门这边可以将上面的加减乘除的例子做一下改造,因为panic之后程序就会终止了,我们可以将错误信息直接返回出来,让程序继续执行

代码语言:javascript
复制
func eval(a int, b int, op string) (int, error) {

	switch op {

		case "+":
			return a + b, nil
		case "-":
			return a - b, nil
		case "*":
			return a * b, nil
		case "/":
			return a / b, nil
		default:
			return 0, fmt.Errorf("unsupported op")
	}
}

3、函数可作为参数

代码语言:javascript
复制
func apply(op func(int, int) int, a, b int) int {
	return op(a, b)
}

Go语言定义这种函数在前,参数在后的复合函数非常的方便,只需要apply一个函数就可以了,当然在现实的过程中有时候也会了偷下懒,相关的op函数就直接写成一个匿名函数了

代码语言:javascript
复制
fmt.Println("sub(3, 4) is:", apply(
	func(a int, b int) int {
		return a - b
	}, 3, 4))

这样也是OK的

4、没有默认参数、没有可选参数

Go语言中没有其他语言类似Lambda这种很花哨的用法,除了一个可变参数列表

代码语言:javascript
复制
func sum(numbers ...int) int {
	s := 0
	for i := range numbers {
		s += numbers[i]
	}
	return s
}

上面就是一个参数求和函数

1.7、指针

1、指针不能运算

比如想对指针做加1运算,Go语言是不支持的;当然要是想在函数内部改变函数外面的变量的值,通过指针是如何实现的呢,如下图所示

2、Go语言只有值传递

Go语言中想要改变变量的值,只能传一个指针进去,比如常见 a b 两个变量的值交换

代码语言:javascript
复制
func swap(a, b int) {
    *a, *b = *b, *a
}

当然呢,交换参数值是不建议上面的写法的

2. 内建容器

2.0、数组

1、定义

代码语言:javascript
复制
var arr1 [5]int
arr2 := [3]int{1, 3, 5}
arr3 := [...]int{2, 4, 6, 8, 10}
var grid [4][5]int

数组的定义和变量的定义类似,数组名在前类型在后;

常规的遍历操作也是类似

代码语言:javascript
复制
for i, v := range arr {
	fmt.Println(i, v)
}

i 是数组的下标,v是数组的值

2、数组是值类型

和上面值传递的概念类似,通过传参在函数内部是改变不了数组的值的;当然要是想改变相关的数组的值,可以通过指针来改变的。接下来的Slice可以直接解决上述的问题。

2.1、Slice(切片)的概念

1、Slice定义

Slice是什么呢?其实呢就是数组的一个View(视图),先来段代码热个身

代码语言:javascript
复制
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

fmt.Println("arr[2:6] =", arr[2:6])
fmt.Println("arr[:6] =", arr[:6])

结果输出:

代码语言:javascript
复制
arr[2:6] = [2 3 4 5]
arr[:6] = [0 1 2 3 4 5]

从上面的输出结果可以直接的看出,arr加一个下标区间都叫做Slice,Slice的区间是一个左闭右开的区间

当然我们还需要知道一个概念,Slice是没有数据的,是对底层Array的一个View,如何理解这个概念呢?简单的用一个例子来理解它

代码语言:javascript
复制
package main

import "fmt"

func updateSliceData(s []int) {
	s[0] = 666
}

func main() {
	arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

	s1 := arr[2:]
	fmt.Println("s1 =", s1)
	s2 := arr[:]
	fmt.Println("s2 =", s2)

	fmt.Println("更新Slice数据 s1")
	updateSliceData(s1)
	fmt.Println(s1)
	fmt.Println(arr)

	fmt.Println("更新Slice数据 s2")
	updateSliceData(s2)
	fmt.Println(s2)
	fmt.Println(arr)
}

结果输出为:

代码语言:javascript
复制
s1 = [2 3 4 5 6 7]
s2 = [0 1 2 3 4 5 6 7]
更新Slice数据 s1
[666 3 4 5 6 7]
[0 1 666 3 4 5 6 7]
更新Slice数据 s2
[666 1 666 3 4 5 6 7]
[666 1 666 3 4 5 6 7]

2、ReSlice

就是在一个Slice上进一步slice,比如

代码语言:javascript
复制
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
ss := arr[:6]
ss = ss[:5]
ss = ss[2:]

结果输出:

代码语言:javascript
复制
[0 1 2 3 4]
[2 3 4]

3、Slice拓展

首先我们先看一个例子

代码语言:javascript
复制
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]

大家或许会有疑问,这个s2不会报错么,要是不报错结果又是多少呢?

代码语言:javascript
复制
[2 3 4 5]
[5 6]

答案是可以,上述就是s1、s2的值,是不是跟你想的有点不一样。那么这又是为什么呢?

这就是为什么能把 6 这个值取出来的原因,因为slice是array的底层的一个view,是不是依然还是有点懵,具体又是如何实现的呢?

4、Slice实现

从上图是不是大体明白为什么上面那个例子能把6取出来了;看到这里大家也能大体明白Slice内部的ptr、len、cap是什么意思,ptr指向slice的开头的元素,len是slice的长度,cap代表底层的array从ptr开始到结束的长度,Slice是可以向后扩展的,但是不能向前扩展,所以只要不超过cap的长度slice都是可以扩展的,但是常规的si取值是不可以超过len的。

用一个例子来简单的理解一下

代码语言:javascript
复制
arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
fmt.Printf("len(s1): %d   ; cap(s1): %d ", len(s1), cap(s1))

输出结果:

代码语言:javascript
复制
len(s1): 4   ; cap(s1): 6 

2.2、Slice(切片)的操作

1、向Slice添加元素

代码语言:javascript
复制
s3 := append(s2, 8)
s4 := append(s3, 9)
s5 := append(s4, 10)
fmt.Println("s3, s4, s5 =", s3, s4, s5)
fmt.Println("arr =", arr)

上面的这个例子打印出来结果又是多少呢?

代码语言:javascript
复制
s3, s4, s5 = [5 6 8] [5 6 8 9] [5 6 8 9 10]
arr = [0 1 2 3 4 5 6 8]

上面的9 ,10为什么不见了呢?因为Go语言在append数据超过cap长度的时候会分配一个更长的数组,如果arr不再使用的话就会被垃圾回收掉。

在append的过程中,由于是值传递的关系,len、cap都有可能会改变,所以呢必须要用一个新的slice来接收这个slice,通常会写成

代码语言:javascript
复制
s = append(s, value1)

2、创建slice

当然slice也可以直接通过var关键字创建

代码语言:javascript
复制
var s []int 

这样创建的slice的初始值就是nil,别的语言中的null的意思,当然也是可以赋初值的,比如:

代码语言:javascript
复制
s1 := []int{2, 4, 6, 8}

就上面的Zero Value的Slice的情况,要是我这个时候对这个slice进行append操作会怎么样呢?这个slice的内部的len以及cap又是如何变化的呢?

代码语言:javascript
复制
var s []int 
for i := 0; i < 100; i++ {
	fmt.Printf("%v, len = %d, cap = %d\n", s, len(s), cap(s))
	s = append(s, 2*i+1)
}

结果我就不输出了,因为相对太长,我把相应的结果总结一下,就是len就是一个步长为1由1增至100,cap呢?当系统发现不够存储的时候会分配一个现有长度两倍的空间。

当然在实际生产过程中,大多是使用的make关键字来创建slice的

代码语言:javascript
复制
s2 := make([]int, 4)
s3 := make([]int, 8, 16)

3、Copy Slice数据

代码语言:javascript
复制
func Copy(dst Writer, src Reader) (written int64, err error)

文档中可以看的很清晰,直接将第二个参数直接拷贝进第一个参数

代码语言:javascript
复制
s1 := []int{2, 4, 6, 8}
s2 := make([]int, 16)
copy(s2, s1)
fmt.Println(s2)

结果输出

代码语言:javascript
复制
[2 4 6 8 0 0 0 0 0 0 0 0 0 0 0 0]

4、Slice删除元素

代码语言:javascript
复制
s1 := []int{2, 4, 6, 8}
s2 := make([]int, 16)
copy(s2, s1)

比如我要删除 s2 中的第 3 个元素该如何操作呢?

代码语言:javascript
复制
s2 = append(s2[:2], s2[3:]...)

当然现实的使用中还会从slice中pop一个值出来,下面分别演示一下从s2头部pop和从s2尾部pop数据

代码语言:javascript
复制
front := s2[0]
s2 = s2[1:]

代码语言:javascript
复制
tail := s2[len(s2)-1]
s2 = s2[:len(s2)-1]

2.3、Map

1、创建map

代码语言:javascript
复制
var m1 map[string]int
m2 := make(map[string]int) 

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

代码语言:javascript
复制
for k, v := range m {
	fmt.Println(k, v)
}

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

2、获取map元素

mkey 一般就是这样获取map的值

代码语言:javascript
复制
var map1 = map[string]string{

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

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

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

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

代码语言:javascript
复制
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 的值

代码语言:javascript
复制
delete(map1, "city")

直接调用就可以

4、map的key

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

2.4、字符和字符串处理

1、rune介绍

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

代码语言:javascript
复制
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)
}

输出结果

代码语言:javascript
复制
你好,杭州
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长度统计

代码语言:javascript
复制
count := utf8.RuneCountInString(s1)
fmt.Println("Rune Count :", count)

输出结果为:

代码语言:javascript
复制
Rune Count : 5

字符串的输出操作

代码语言:javascript
复制
bytes := []byte(s1)
for len(bytes) > 0 {
	ch, size := utf8.DecodeRune(bytes)
	bytes = bytes[size:]
	fmt.Printf("%c ", ch)
}

用rune实现上述同样的功能

代码语言:javascript
复制
for _, ch := range []rune(s1) {
	fmt.Printf("%c ", ch)
}

3. 面向“对象”

3.0、结构体和方法

1、结构体的创建

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

下面先来介绍一下struct的创建:

代码语言:javascript
复制
type Node struct {
	Value       int
	Left, Right *Node
}

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

代码语言:javascript
复制
var root tree.Node
root = tree.Node{Value:1}
root.Value = 2
root.Left = &tree.Node{Value:3}
root.Right = &tree.Node{}

2、方法创建

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

代码语言:javascript
复制
func (node Node) Print() {
	fmt.Print(node.Value, " ")
}

上述就是一个值接收者打印出Node的Value的值的方法。

当然要是需要改变Value的值的时候,就需要一个指针接收者。

代码语言:javascript
复制
func (node *Node) SetValue(value int) {
	node.Value = value
}

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

代码语言:javascript
复制
var pRoot *tree.Node
pRoot.SetValue(1)

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

代码语言:javascript
复制
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)

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

代码语言:javascript
复制
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新特性)

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

基本语法就是:

代码语言:javascript
复制
type identifier = Type

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

代码语言:javascript
复制
// 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

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

代码语言:javascript
复制
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、使用组合

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

代码语言:javascript
复制
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呢?

代码语言:javascript
复制
~ sheng$ echo $GOPATH
/Users/verton/GoLangProject

1、go get获取第三方库

代码语言:javascript
复制
go get url

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

代码语言:javascript
复制
sheng$ go get github.com/gpmgo/gopm

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

代码语言:javascript
复制
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、接口的定义

代码语言:javascript
复制
type Retriever interface{
    Get(url string) string
}

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

2、接口的实现

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

代码语言:javascript
复制
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)
}

代码语言:javascript
复制
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、接口变量里面有什么

继续使用上面的例子

代码语言:javascript
复制
var r Retriever
r = rick.Retriever{}

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

会输出什么呢?

代码语言:javascript
复制
rick.Retriever {} 

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

代码语言:javascript
复制
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)
}

代码语言:javascript
复制
var r Retriever
r = &rick.Retriever{}

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

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

代码语言:javascript
复制
*rick.Retriever &{} 

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

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

2、查看接口变量

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

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

代码语言:javascript
复制
var a interface{}
fmt.Println("Are you ok?", a.(string))

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

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

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

代码语言:javascript
复制
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()函数可以达到想要的效果。

4.3、接口的组合

1、定义

什么叫接口的组合?当然这就是它的字面上的意思,接口可以组合其他的接口。这种方式等效于在接口中添加其他的接口的方法。在系统函数中就有很多这样的组合,比如:ReadWriter

代码语言:javascript
复制
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
	Reader
	Writer
}

在常见的读写文件的时候,网络相关以及一些底层的东西经常会遇到 Reader 、Writer

2、实例演示

为了更好的理解接口的组合的概念,下面用一个简单的例子来进一步了解

代码语言:javascript
复制
// 定义Reader接口
type Reader interface {
	read()
}
// 定义Writer接口
type Writer interface {
	write()
}
// 实现上述两个接口
type myReaderWriter struct {
}

func (mrw *myReaderWriter) read()  {
	fmt.Println("myReaderWriter read func...")
}

func (mrw *myReaderWriter) write() {
	fmt.Println("myReadWriter writer func...")
}
// 定义一个接口,组合上述两个接口
type ReaderWriterV1 interface {
	Reader
	Writer
}
// 等价于
type ReaderWriterV2 interface {
	read()
	write()
}

func main() {
	mrw := &myReaderWriter{}
	//mrw对象实现了read()方法和write()方法,因此可以赋值给ReaderWriterV1和ReaderWriterV2
	var rwv1 ReaderWriterV1 = mrw
	rwv1.read()
	rwv1.write()

	var rwv2 ReaderWriterV2 = mrw
	rwv2.write()
	rwv2.read()

	//同时,ReaderWriterV1和ReaderWriterV2两个接口对象可以相互赋值
	rwv1 = rwv2
	rwv2 = rwv1
}

4.4、常用的系统接口

1、Stringer

这个就是常见的 toString 的功能,

代码语言:javascript
复制
// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
	String() string
}

Stringer接口定义在fmt包中,该接口包含String()函数。任何类型只要定义了String()函数,进行Print输出时,就可以得到定制输出。比如:

代码语言:javascript
复制
package main

import "fmt"

type Person struct{
	age int
	gender string
	name string
}

func (p Person) String() string {
	return fmt.Sprintf("age:%d, gender:%s, name:%s", p.age, p.gender, p.name)
}

func main() {
	var i Person = Person{
		age: 25,
		gender: "male",
		name: "sheng.guocun",
	}
	fmt.Printf("%s\n", i)
	fmt.Println(i)
	fmt.Printf("%v", i)
}

结果输出为:

代码语言:javascript
复制
age:25, gender:male, name:sheng.guocun
age:25, gender:male, name:sheng.guocun
age:25, gender:male, name:sheng.guocun

2、Reader、Writer

Reader Writer 上面有提到过,就是常见的读写文件的时候经常会用到,就是对文件的一个抽象,但是不仅这些,比如常见的

代码语言:javascript
复制
// NewScanner returns a new Scanner to read from r.
// The split function defaults to ScanLines.
func NewScanner(r io.Reader) *Scanner {
	return &Scanner{
		r:            r,
		split:        ScanLines,
		maxTokenSize: MaxScanTokenSize,
	}
}

这的参数也是一个Reader,还有很多的底层的代码都是基于 Reader Writer 的,这里就不一一举例了。

欢迎关注 个人微信公众号

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 0. 前言
  • 1. 基础
    • 1.0、环境搭建
      • 1、下载安装包安装
      • 2、命令行安装
    • 1.1、变量定义
      • 1、通过var关键字
      • 2、使用 := 定义变量
    • 1.2、内建变量类型
      • 1、bool 、string
      • 2、(u)int、(u)int8、(u)int16、(u)int32、(u)int64、uintptr
      • 3、byte、rune
      • 4、float32、float64、complex64、complex128
    • 1.3、常量与枚举
      • 1.4、条件语句
        • 1、if
        • 2、switch
      • 1.5、循环
        • 1、for
      • 1.6、函数
        • 1、普通函数
        • 2、多返回值
        • 3、函数可作为参数
        • 4、没有默认参数、没有可选参数
      • 1.7、指针
        • 1、指针不能运算
        • 2、Go语言只有值传递
    • 2. 内建容器
      • 2.0、数组
        • 1、定义
        • 2、数组是值类型
      • 2.1、Slice(切片)的概念
        • 1、Slice定义
        • 2、ReSlice
        • 3、Slice拓展
        • 4、Slice实现
      • 2.2、Slice(切片)的操作
        • 1、向Slice添加元素
        • 2、创建slice
        • 3、Copy Slice数据
        • 4、Slice删除元素
      • 2.3、Map
        • 1、创建map
        • 2、获取map元素
        • 3、删除元素
        • 4、map的key
      • 2.4、字符和字符串处理
        • 1、rune介绍
        • 2、字符串处理
    • 3. 面向“对象”
      • 3.0、结构体和方法
        • 1、结构体的创建
        • 2、方法创建
      • 3.1、包和封装
        • 1、命名规范
        • 2、包的概念
      • 3.2、扩展已有类型
        • 1、定义别名(1.9新特性)
        • 2、使用组合
      • 3.3、GOPATH以及目录结构
        • 1、go get获取第三方库
        • 2、目录结构
    • 4. 面向接口
      • 4.0、Duck Typing的概念
        • 4.1、接口的定义和实现
          • 1、接口的定义
          • 2、接口的实现
        • 4.2、接口值的类型
          • 1、接口变量里面有什么
          • 2、查看接口变量
        • 4.3、接口的组合
          • 1、定义
          • 2、实例演示
        • 4.4、常用的系统接口
          • 1、Stringer
          • 2、Reader、Writer
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档