在本篇文章中,我会对 Go 语言编程模式的一些基本技术和要点,这样可以让你更容易掌握 Go 语言编程。其中,主要包括,数组切片的一些小坑,还有接口编程,以及时间和程序运行性能相关的话题。

本文是全系列中第 1 / 9 篇:Go 编程模式[1]

Go 编程模式:切片,接口,时间和性能

Go 编程模式:错误处理

[2]

Go 编程模式:Functional Options

[3]

Go 编程模式:委托和反转控制

[4]

Go 编程模式:Map-Reduce

[5]

Go 编程模式:Go Generation

[6]

Go 编程模式:修饰器

[7]

Go 编程模式:Pipeline

[8]

Go 编程模式:k8s Visitor 模式

[9]

1. Slice

首先,我们先来讨论一下 Slice,中文翻译叫“切片”,这个东西在 Go 语言中不是数组,而是一个结构体,其定义如下:

type slice struct {

array unsafe.Pointer //指向存放数据的数组指针

len   int            //长度有多大

cap   int            //容量有多大

}

用图示来看,一个空的 slice 的表现如下:

熟悉 C/C++的同学一定会知道,在结构体里用数组指针的问题——数据会发生共享!下面我们来看一下 slice 的一些操作

foo = make([]int, 5)

foo[3] = 42

foo[4] = 100

bar  := foo[1:4]

bar[1] = 99

对于上面这段代码。

首先先创建一个 foo 的 slice,其中的长度和容量都是 5

然后开始对 foo 所指向的数组中的索引为 3 和 4 的元素进行赋值

然后,对 foo 做切片后赋值给 bar,再修改 bar[1]

通过上图我们可以看到,因为 foo 和 bar 的内存是共享的,所以,foo 和 bar 的对数组内容的修改都会影响到对方。

接下来,我们再来看一个数据操作 append() 的示例

a := make([]int, 32)

b := a[1:16]

a = append(a, 1)

a[2] = 42

上面这段代码中,把 a[1:16] 的切片赋给到了 b ,此时,a 和 b 的内存空间是共享的,然后,对 a做了一个 append()的操作,这个操作会让 a 重新分享内存,导致 a 和 b 不再共享,如下图所示:

从上图我们可以看以看到 append()操作让 a 的容量变成了 64,而长度是 33。这里,需要重点注意一下——append()这个函数在 cap 不够用的时候就会重新分配内存以扩大容量,而如果够用的时候不不会重新分享内存!

我们再看来看一个例子:

func main() {

path := []byte("AAAA/BBBBBBBBB")

sepIndex := bytes.IndexByte(path,'/’)

dir1 := path[:sepIndex]

dir2 := path[sepIndex+1:]

fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA

fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

dir1 = append(dir1,"suffix"...)

fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix

fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB

}

上面这个例子中,dir1 和 dir2 共享内存,虽然 dir1 有一个 append() 操作,但是因为 cap 足够,于是数据扩展到了dir2 的空间。下面是相关的图示(注意上图中 dir1 和 dir2 结构体中的 cap 和 len 的变化)

如果要解决这个问题,我们只需要修改一行代码。

dir1 := path[:sepIndex]

修改为

dir1 := path[:sepIndex:sepIndex]

新的代码使用了 Full Slice Expression,其最后一个参数叫“Limited Capacity”,于是,后续的 append() 操作将会导致重新分配内存。

2. 深度比较

当我们复杂一个对象时,这个对象可以是内建数据类型,数组,结构体,map……我们在复制结构体的时候,当我们需要比较两个结构体中的数据是否相同时,我们需要使用深度比较,而不是只是简单地做浅度比较。这里需要使用到反射 reflect.DeepEqual() ,下面是几个示例

import (

"fmt"

"reflect"

)

func main() {

v1 := data{}

v2 := data{}

fmt.Println("v1 == v2:",reflect.DeepEqual(v1,v2))

//prints: v1 == v2: true

m1 := map[string]string{"one": "a","two": "b"}

m2 := map[string]string{"two": "b", "one": "a"}

fmt.Println("m1 == m2:",reflect.DeepEqual(m1, m2))

//prints: m1 == m2: true

s1 := []int{1, 2, 3}

s2 := []int{1, 2, 3}

fmt.Println("s1 == s2:",reflect.DeepEqual(s1, s2))

//prints: s1 == s2: true

}

3. 接口编程

下面,我们来看段代码,其中是两个方法,它们都是要输出一个结构体,其中一个使用一个函数,另一个使用一个“成员函数”。

func PrintPerson(p *Person) {

fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",

p.Name, p.Sexual, p.Age)

}

func (p *Person) Print() {

fmt.Printf("Name=%s, Sexual=%s, Age=%d\n",

p.Name, p.Sexual, p.Age)

}

func main() {

var p = Person{

Name: "Hao Chen",

Sexual: "Male",

Age: 44,

}

PrintPerson(&p)

p.Print()

}

你更喜欢哪种方式呢?在 Go 语言中,使用“成员函数”的方式叫“Receiver”,这种方式是一种封装,因为 PrintPerson()本来就是和 Person强耦合的,所以,理应放在一起。更重要的是,这种方式可以进行接口编程,对于接口编程来说,也就是一种抽象,主要是用在“多态”,这个技术,在《Go 语言简介(上):接口与多态[10]》中已经讲过。在这里,我想讲另一个 Go 语言接口的编程模式。

首先,我们来看一下,有下面这段代码:

type Country struct {

Name string

}

type City struct {

Name string

}

type Printable interface {

PrintStr()

}

func (c Country) PrintStr() {

fmt.Println(c.Name)

}

func (c City) PrintStr() {

fmt.Println(c.Name)

}

c1 := Country {"China"}

c2 := City {"Beijing"}

c1.PrintStr()

c2.PrintStr()

其中,我们可以看到,其使用了一个 Printable 的接口,而 Country 和 City 都实现了接口方法 PrintStr() 而把自己输出。然而,这些代码都是一样的。能不能省掉呢?

我们可以使用“结构体嵌入”的方式来完成这个事,如下的代码所示:

type WithName struct {

Name string

}

type Country struct {

WithName

}

type City struct {

WithName

}

type Printable interface {

PrintStr()

}

func (w WithName) PrintStr() {

fmt.Println(w.Name)

}

c1 := Country {WithName{ "China"}}

c2 := City { WithName{"Beijing"}}

c1.PrintStr()

c2.PrintStr()

引入一个叫 WithName的结构体,然而,所带来的问题就是,在初始化的时候,变得有点乱。那么,我们有没有更好的方法?下面是另外一个解。

type Country struct {

Name string

}

type City struct {

Name string

}

type Stringable interface {

ToString() string

}

func (c Country) ToString() string {

return "Country = " + c.Name

}

func (c City) ToString() string{

return "City = " + c.Name

}

func PrintStr(p Stringable) {

fmt.Println(p.ToString())

}

d1 := Country {"USA"}

d2 := City{"Los Angeles"}

PrintStr(d1)

PrintStr(d2)

上面这段代码,我们可以看到——**我们使用了一个叫Stringable 的接口,我们用这个接口把“业务类型” Country 和 City 和“控制逻辑” Print() 给解耦了。**于是,只要实现了Stringable 接口,都可以传给 PrintStr() 来使用。

这种编程模式在 Go 的标准库有很多的示例,最著名的就是 io.Read 和 ioutil.ReadAll 的玩法,其中 io.Read 是一个接口,你需要实现他的一个 Read(p []byte) (n int, err error) 接口方法,只要满足这个规模,就可以被 ioutil.ReadAll这个方法所使用。这就是面向对象编程方法的黄金法则——“Program to an interface not an implementation”

4. 接口完整性检查

另外,我们可以看到,Go 语言的编程器并没有严格检查一个对象是否实现了某接口所有的接口方法,如下面这个示例:

type Shape interface {

Sides() int

Area() int

}

type Square struct {

len int

}

func (s* Square) Sides() int {

return 4

}

func main() {

s := Square{len: 5}

fmt.Printf("%d\n",s.Sides())

}

我们可以看到 Square 并没有实现 Shape 接口的所有方法,程序虽然可以跑通,但是这样编程的方式并不严谨,如果我们需要强制实现接口的所有方法,那么我们应该怎么办呢?

在 Go 语言编程圈里有一个比较标准的作法:

var _ Shape = (*Square)(nil)

声明一个 _ 变量(没人用),其会把一个 nil 的空指针,从 Square 转成 Shape,这样,如果没有实现完相关的接口方法,编译器就会报错:

cannot use (*Square)(nil) (type *Square) as type Shape in assignment: *Square does not implement Shape (missing Area method)

这样就做到了个强验证的方法。

5. 时间

对于时间来说,这应该是编程中比较复杂的问题了,相信我,时间是一种非常复杂的事(比如《你确信你了解时间吗?[11]》、《关于闰秒[12]》等文章)。而且,时间有时区、格式、精度等等问题,其复杂度不是一般人能处理的。所以,一定要重用已有的时间处理,而不是自己干。

在 Go 语言中,你一定要使用 time.Time 和 time.Duration 两个类型:

在命令行上,

flag 通过

time.ParseDuration 支持了

time.Duration

JSon 中的

encoding/json 中也可以把

time.Time 编码成

RFC 3339

[13] 的格式

数据库使用的

database/sql 也支持把

DATATIME 或

TIMESTAMP 类型转成

time.Time

YAML 你可以使用

gopkg.in/yaml.v2 也支持

time.Time 、

time.Duration 和

RFC 3339

[14] 格式

如果你要和第三方交互,实在没有办法,也请使用 RFC 3339[15] 的格式。

最后,如果你要做全球化跨时区的应用,你一定要把所有服务器和时间全部使用 UTC 时间。

6. 性能提示

Go 语言是一个高性能的语言,但并不是说这样我们就不用关心性能了,我们还是需要关心的。下面是一个在编程方面和性能相关的提示。

如果需要把数字转字符串,使用

strconv.Itoa() 会比

fmt.Sprintf() 要快一倍左右

尽可能地避免把

String转成

[]Byte 。这个转换会导致性能下降。

如果在 for-loop 里对某个 slice 使用

append()请先把 slice 的容量很扩充到位,这样可以避免内存重新分享以及系统自动按 2 的 N 次方幂进行扩展但又用不到,从而浪费内存。

使用

StringBuffer 或是

StringBuild 来拼接字符串,会比使用

+ 或

+= 性能高三到四个数量级。

尽可能的使用并发的 go routine,然后使用

sync.WaitGroup 来同步分片操作

避免在热代码中进行内存分配,这样会导致 gc 很忙。尽可能的使用

sync.Pool 来重用对象。

使用 lock-free 的操作,避免使用 mutex,尽可能使用

sync/Atomic包。(关于无锁编程的相关话题,可参看《

无锁队列实现

[16]》或《

无锁 Hashmap 实现

[17]》)

使用 I/O 缓冲,I/O 是个非常非常慢的操作,使用

bufio.NewWrite() 和

bufio.NewReader() 可以带来更高的性能。

对于在 for-loop 里的固定的正则表达式,一定要使用

regexp.Compile() 编译正则表达式。性能会得升两个数量级。

如果你需要更高性能的协议,你要考虑使用

protobuf

[18] 或

msgp

[19] 而不是 JSON,因为 JSON 的序列化和反序列化里使用了反射。

你在使用 map 的时候,使用整型的 key 会比字符串的要快,因为整型比较比字符串比较要快。

参考

还有很多不错的技巧,下面的这些参考文档可以让你写出更好的 Go 的代码,必读!

Effective Go

[20]

Uber Go Style

[21]

50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs

[22]

Go Advice

[23]

Practical Go Benchmarks

[24]

Benchmarks of Go serialization methods

[25]

Debugging performance issues in Go programs

[26]

Go code refactoring: the 23x performance hunt

[27]

参考资料

[1]

Go 编程模式: https://coolshell.cn/articles/series/go编程模式

[2]

Go 编程模式:错误处理: https://coolshell.cn/articles/21140.html

[3]

Go 编程模式:Functional Options: https://coolshell.cn/articles/21146.html

[4]

Go 编程模式:委托和反转控制: https://coolshell.cn/articles/21214.html

[5]

Go 编程模式:Map-Reduce: https://coolshell.cn/articles/21164.html

[6]

Go 编程模式:Go Generation: https://coolshell.cn/articles/21179.html

[7]

Go 编程模式:修饰器: https://coolshell.cn/articles/17929.html

[8]

Go 编程模式:Pipeline: https://coolshell.cn/articles/21228.html

[9]

Go 编程模式:k8s Visitor 模式: https://coolshell.cn/articles/21263.html

[10]

Go 语言简介(上):接口与多态: https://coolshell.cn/articles/8460.html#接口和多态

[11]

你确信你了解时间吗?: https://coolshell.cn/articles/5075.html

[12]

关于闰秒: https://coolshell.cn/articles/7804.html

[13]

RFC 3339: https://tools.ietf.org/html/rfc3339

[14]

RFC 3339: https://tools.ietf.org/html/rfc3339

[15]

RFC 3339: https://tools.ietf.org/html/rfc3339

[16]

无锁队列实现: https://coolshell.cn/articles/8239.html

[17]

无锁 Hashmap 实现: https://coolshell.cn/articles/9703.html

[18]

protobuf: https://github.com/golang/protobuf

[19]

msgp: https://github.com/tinylib/msgp

[20]

Effective Go: https://golang.org/doc/effective_go.html

[21]

Uber Go Style: https://github.com/uber-go/guide/blob/master/style.md

[22]

50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs: http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/

[23]

Go Advice: https://github.com/cristaloleg/go-advice

[24]

Practical Go Benchmarks: https://www.instana.com/blog/practical-golang-benchmarks/

[25]

Benchmarks of Go serialization methods: https://github.com/alecthomas/go_serialization_benchmarks

[26]

Debugging performance issues in Go programs: https://github.com/golang/go/wiki/Performance

[27]

Go code refactoring: the 23x performance hunt: https://medium.com/@val_deleplace/go-code-refactoring-the-23x-performance-hunt-156746b522f7

golang 切片 接口_Go编程模式:切片,接口,时间和性能相关推荐

  1. matlab接口与编程,精通MATLAB接口与编程

    基本信息 书名:精通MATLAB接口与编程 定价:49.00元 作者: 出版社:电子工业出版社 出版日期:2007-01-00 ISBN:9787121036576 字数: 页码: 版次: 装帧: 开 ...

  2. 网上购物系统(Task100)——业务逻辑层BLL(面向接口的编程模式)

    源代码:13033480群共享 [操作步骤] 一.新建类库IDAL,设置属性,添加引用→项目→Model 二.添加类ICategory.cs和IItem.cs 1.ICategory.cs using ...

  3. Go编程模式:切片,接口,时间和性能

    更多奇技淫巧欢迎订阅博客:https://fuckcloudnative.io 前言 在本篇文章中,我会对 Go 语言编程模式的一些基本技术和要点,这样可以让你更容易掌握 Go 语言编程.其中,主要包 ...

  4. java 接口编程_JAVA面向接口编程

    一.什么是面向接口编程 要正确地使用Java语言进行面向对象的编程,从而提高程序的复用性,增加程序的可维护性.可扩展性,就必须是面向接口的编程.面向接口的编程就意味着:开发系统时,主体构架使用接口,接 ...

  5. golang 切片 接口_Go 经典入门系列 18:接口(一)

    点击上方蓝色"Go语言中文网"关注,每天一起学 Go 欢迎来到 Golang 系列教程[1]的第 18 个教程.接口共有两个教程,这是我们接口的第一个教程. 什么是接口? 在面向对 ...

  6. Golang面向API编程-interface(接口)

    Golang面向API编程-interface(接口) 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. Golang并不是一种典型的面向对象编程(Object Oriented Pr ...

  7. 换个语言学一下 Golang (9)——结构体和接口

    换个语言学一下 Golang (9)--结构体和接口 基本上到这里的时候,就是上了一个台阶了.Go的精华特点即将展开. 结构体定义 上面我们说过Go的指针和C的不同,结构体也是一样的.Go是一门删繁就 ...

  8. FPGA之道(58)关于外界接口的编程思路

    文章目录 前言 关于外界接口的编程思路 按传递方向分类 输入接口 输出接口 双向接口 原理简介 工作模式 主从模式 对等模式 简单示例 按电气特性分类 单端接口 差分接口 无线接口 按功能特性分类 时 ...

  9. MyBatis的接口式编程Demo

    很久没细看过MyBatis了,时间一长就容易忘记. 下面是一个接口式编程的例子. 这里的例子一共分为4步: 1 首先要有一个namespace为接口的全类名的映射文件,该例中是 IMyUser.xml ...

最新文章

  1. python中的逻辑运算符and和or
  2. String spilt时转义特殊字符【转】
  3. linux: convmv =-======pkgs.org
  4. boost::rational用法的测试程序
  5. 提取网页里面全部所有链接的方法
  6. android webView的使用
  7. dicom文件的后缀_dcm文件扩展名,dcm文件怎么打开?
  8. Windows Kernel Programming Windows内核编程(一)
  9. 【GIS】GIS矢量空间分析(上)
  10. 城市规划CAD影像底图快速导入方法
  11. 使用破解补丁破解photoshop cs6
  12. 百宝云数组语法新手教程
  13. 【一起入门NLP】中科院自然语言处理作业三:用BiLSTM+CRF实现中文命名实体识别(TensorFlow入门)【代码+报告】
  14. 工作人员做好项目协调服务器,项目团队协作做好三件事
  15. 女程序猿做了个梦,各路大神惊现神级评论!
  16. SAAS平台的基础,构建多租户系统的思考
  17. 通过K-means对iris数据集进行处理 Kmeans聚类算法实例
  18. js中元素样式设置的六种方法
  19. 打开控制台查看后台html代码
  20. Selenium:动态页面模拟点击

热门文章

  1. java外部接口图解_java代码实现访问网络外部接口并获取数据的工具类详解
  2. php mysql 失败_在php中插入失败的数据mysql
  3. Linux图片马PHP,php 根据请求生成缩略图片保存到Linux图片服务器的代码
  4. mysql exists in join_子查询、left join、 exists 替代 not in
  5. libreoffice Error: source file could not be loaded
  6. 解决Windows中PLSQL连接虚拟机中Oracle缓慢的问题
  7. 工作流实战_03_flowable 流程模板部署
  8. 汉字转html实体符号js_html实体编码遇上js代码
  9. 手机站的view.php,织梦网站文章内容页动态地址plus/view.php路径修改
  10. mongodb php代码实例,php操作mongoDB实例分析