16.10.13Docker最新动态 Docker在中国找到了第一个官方合作伙伴-阿里云

wwdyy 发表了文章 • 2 个评论 • 430 次浏览 • 2016-10-13 19:16 • 来自相关话题

10月13日,在2016杭州·云栖大会上,全球知名的容器技术公司Docker与阿里云宣布达成战略合作,双方将在容器服务领域进行紧密合作,阿里云将为客户提供更加先进的云上应用管理服务。 查看全部

10月13日,在2016杭州·云栖大会上,全球知名的容器技术公司Docker与阿里云宣布达成战略合作,双方将在容器服务领域进行紧密合作,阿里云将为客户提供更加先进的云上应用管理服务。


基于合作,双方在开源容器技术以及发展方向上共同努力,并提供本地化的Docker服务。Docker公司选择阿里云平台作为其Docker Hub在中国运营的基础服务。阿里云也获得Docker Engine商用版以及Docker DataCenter运营权,并为Docker客户提供企业级支持和咨询服务。同时,阿里云将成为Docker官方支持的云服务提供商。


Docker技术是近年来最火的开源技术,正在改变企业应用从开发、构建到发布、运行的整个生命周期。 此前,Docker与AWS、Azure、IBM、HPE进行合作,阿里云是其在国内的第一个官方合作伙伴。


阿里云在2015年开始提供“容器服务”,在容器领域有着长期的技术积累和持续投入。阿里云将为客户提供更为多样的容器相关解决方案如DevOps、微服务改造,藉此帮助企业用户通过更为稳定、高效、安全的实现其对应用升级和改造,有效降低维护成本,交付效率提升13倍。同时,利用容器“一次构建,随处运行”的特点,可以帮助用户迁移无缝应用到云环境。


Docker Hub等服务落地中国,可以更好地服务国内开发者,促进中国技术社区的成长。得益于阿里云强大的云基础设施,将极大提升国内用户对Docker Hub的访问体验。


在阿里云上,华大基因正在利用容器服务构建基因数据的应用开发和共享平台,路特通过阿里云容器服务实现DevOps为企业客户提供SaaS服务,学霸君基于容器服务实现微服务架构应用和DevOps流程优化。


Docker CEO Ben Golub说,我们很高兴把Docker容器技术, 商业化的Docker Engine和Docker DataCenter引进到中国,我们选择了领先的的合作伙伴,让我们能够更好地服务于我们共同的客户。


阿里云总裁胡晓明表示,通过和Docker的战略合作,阿里云将更好地为企业级客户提供完善的云服务,使能客户,并实现时代转型。

Windows 原生 Docker 正式商用

DaoCloud 发表了文章 • 0 个评论 • 585 次浏览 • 2016-10-13 16:59 • 来自相关话题

上图显示:Windows 应用已经可以稳定地运行在原生容器管理平台之上

... 查看全部


上图显示:Windows 应用已经可以稳定地运行在原生容器管理平台之上


2016 年 9 月 26 日,微软 Ignite 技术大会在亚特兰大举行,微软官方正式发布了 Windows Server 2016。对于广大 Windows 开发人员和 IT 技术专家来说,Windows 最令人激动的新功能,非「容器」莫属。而运行在 Windows Server 2016 上的容器,正是由 Docker 公司所驱动。


本篇博客将详细剖析这些让 Docker 容器与 Windows 完美匹配的技术创新点,并尝试阐述这些成就的重要意义。我们也推荐你看看同期发布的几篇博客,其中一篇讲述了如何创建你的第一台 Windows 容器,还有一篇详细介绍了 Docker 公司和微软公司为了在 Windows 上支持 Docker 而开展的一系列商业合作。



2013 年,第一版 Docker 正式问世。从那以后的三年间,Docker 发展势如破竹,彻底改变了 Linux 开发者和运维人员 “构建、交付和运行(build,ship and run)” 应用的方式。如今,Docker Engine 和容器已经可以在 Windows 平台之上原生使用了,开发者和 IT 专家们也可以在 Windows 平台应用和基础架构上,体验一把 Linux 用户曾经经历过的大革新,享受 Linux 用户曾经享受过的福利:更好的安全性、更高的敏捷性、更优良的便捷性,以及随时可以把私有应用迁移到云端之上的灵活性。


而对于那些需要创建和维护 Linux 和 Windows 两大架构平台的开发人员和 IT 专家们来说,Docker 运行在 Windows 平台的重大意义更加不言而喻:现在,Docker 平台已经可以为 Linux 和 Windows 应用的管理提供一整套工具、API 和镜像格式了。随着 Linux 和 Windows 应用与服务器不断地 「Docker 化」,开发者和 IT 专家将能够通过使用 Docker 技术来管理和改进本地和云端的复杂微服务部署。


在 Windows Server 之上运行容器


Docker 和微软已经磨合了两年,期间开展了一系列的合作——Windows 内核中容器化基元(primitives)越来越多,为了充分利用这些新基元,双方协力把 Docker Engine 和 CLI 迁移到 Windows 平台上,Docker 还给 Docker Hub 增加了多架构镜像支持。今天 Windows 平台中正式引入 Docker,正是这两年合作的结晶。


结果就是,现在在 Windows 平台上已经可以完美使用强大无比的 docker run 来快速启动一个完全独立的新容器了。



内核容器化功能已经整合进所有版本的 Windows Server 2016,在 Win10 的周年更新系统中也有。Windows 原生的 Docker daemon 可以在 Windows Server 2016 和 Win10 系统上运行(虽然 Win10 上只能创建和运行基于 Windows Server 的容器)。


Windows 版的 docker run 和 Linux 版有着一样的意义:全进程隔离,自带层级变动支持的沙盒文件系统(还有 Windows 注册表!)。每个容器都只面向一个纯净的 Windows 系统,而且无法介入到系统上的其他进程(不管这个进程是否被容器化了)。


举个例子,两个用着不同网络信息服务(IIS)版本,用着不同 . NET 框架的 Docker 化应用,可以在同一个系统中友好共存,甚至可以在互不影响的情况下,给各自的文件系统和注册表读写数据。


容器化之后,Windows IT 专家们可以隔离全部进程,发布状态的工件将变得非常稳定,用起虚拟机来就更加得心应手了,不必担心在硬件虚拟化过程中产生的资源超支和灵敏度降低。


Linux 上的容器可以用不同的安全档案运行,Windows 上的容器也与此类似,可以运行以下两种隔离模式中的一种:


1、Windows Server 容器使用和 Linux 容器一样的共享内核进程隔离范式。由于容器作为标准(但是隔离)的进程来运行,所以启动很快,而资源超支可以降到最低。


2、有了 Hyper-V 隔离,容器一启动就会生成一个很小的管理程序,容器进程就在这个程序里运行。虽然启动速度会稍慢一点,资源占用也会略有增加,但整体的隔离环境却会好上不少。


可以通过 docker run 上一个简单的开关来设置隔离:



只要底层主机能支持所需的隔离模式,任何 Windows 容器镜像都能当作一台 Hyper-V 或服务器容器来运行,而且一台容器主机可以一个接着一个地运行这两者。基于容器的隔离模式,它是不知道容器进程的,而 Docker 控制 API 对于两种模式而言都是一样的。


这样一来,开发者就无需经常为隔离模式操心了,他们只需要用默认的设置就行,或者自己做些方便上手的设定。当 IT 专家需要选择如何在产品中部署容器化的应用时,隔离模式实实在在地为他们提供了选择的余地。


当然了,有一点要注意:尽管 Hyper-V 是支持 Hyper-V隔离的运行时技术,但 Hyper-V 隔离了的容器并不是 Hyper-V 虚拟机,不能用标准的 Hyper-V 工具来管理。


构建 Windows 容器镜像


得益于 Windows 注册表和文件系统的层级改进,docker build 和 Dockerfiles 都能完全支持创建 Windows Docker 镜像。下面是一个 Windows Dockerfile 样例文件,它是由斯蒂凡·施尔提交给 Node.js 官方 Docker 库的镜像。它能用 docker build 在 Windows 上创建出来:



来看看 PowerShell 是如何用于安装和启动 zip 文件和应用程序的吧:Windows 容器运行遵循 Windows API 的可执行程序。要创建和运行 Windows 容器,你需要一个 Windows 系统。虽然 Windows 和 Linux 上的 Docker 工具、控制 API 和镜像格式都是一样的,Docker Windows 容器不能在 Linux 系统上运行,反之亦然。


还请注意,开始层是 microsoft/windowsservercore。在创建 Windows 容器镜像时,开启 FROMScratch 是没用的。镜像要么是基于 microsoft/windowsserverco,要么是基于 microsoft/nanoserver。


Windows Server Core 镜像还配有一个近乎完整的用户态,这个用户态有各种进程,还有建立在标准 Windows Server Core 安装上的 DLL。除了 GUI 应用,以及那些需要 Windows 远程桌面的应用,大部分运行在 Windows Server 上的应用都能被 Docker 化,以最少的误差,在一个基于 microsoft / windowsservercore 的镜像上运行。举几个例子:MicrosoftSQL Server, Apache, 网络信息服务(IIS),以及整个 .NET 框架。


这种灵活性,是以容量的暴增为代价的:microsoft/windowsservercore 镜像高达 10 个 G。不过幸好有了 Docker 高效率的镜像层级,在实际操作中容量过大并不是什么大麻烦。任何一台 Docker 主机只需要拖进底层一次便足矣,任何拖进系统或创建在系统上的镜像只不过是在重复利用底层。


另一个底层选项是 Nano Server,这是一款全新的小体积 Windows 版本,带有一个精简版的 Windows API。包括 IIS、新版 .NET Core framework,Node.js 和 GO 在内,已经有大量的软件运行在 Nano Server 上了。而且 Nano Server 基本镜像的体积远小于 Windows Server Core,这意味着它的必备组件更少,保持刷新所需的表面积也更小。Nano Server 是一个令人激动的成果,不仅因为,作为小型容器的底层,它的创建和 boot 非常快,还因为,它作为一种极小主义操作系统,是为了另一款同样优秀的,专门运行 Docker 镜像和容器的容器主机 OS 而生的——这就是它的伟大之处。


有了 Windows Server Core 和 Nano Server 可供选择,开发者和 IT 大牛们就可以自由玩转了,要么把异彩纷呈的 Windows 平台应用 “lift-and-shift” 到 Server Core 容器里,要么采用 Nano Server 搞绿地模式开发,要么把整个应用分解成数量更多的小部分,整合进微服务的组件里。


Docker 目前正与微软及其社区携手,在 WindowsServer Core 和 Nano Server 上创建容器镜像。Golang、Python 和 Mongo 语言作为正式 Docker 镜像,都可以用,更多的 Docker 镜像也正在开发之中,而且微软还维护了一系列非常普及的样本镜像。


总结


今天,Docker Engine 能在 Windows 系统上创建、运行和管理容器,是微软团队、Docker 团队,以及 Docker 社区成员经年累月劳动的成果。Docker 与微软通力合作,把容器化的福音带给了 Windows 开发者和 IT专家,我们为此感到无比自豪;让 Windows 和 Linux 技术能用同一套工具和 API 来创建、交付和运行应用,我们也为此感到无比激动。

Docker源码分析,附阅读地址

wwdyy 发表了文章 • 3 个评论 • 484 次浏览 • 2016-10-13 11:26 • 来自相关话题

Docker是一个由GO语言写的程序运行的“容器”,开发者可以从操作系统到依赖库乱七八糟东西(tomcat,mysql等等)全给你“静态编译”到一起变成一容器中

即使不在项目中使用 Docker,本书也能够为 Go 语言程序员带来帮助。Do... 查看全部

Docker是一个由GO语言写的程序运行的“容器”,开发者可以从操作系统到依赖库乱七八糟东西(tomcat,mysql等等)全给你“静态编译”到一起变成一容器中


即使不在项目中使用 Docker,本书也能够为 Go 语言程序员带来帮助。Docker 项目
中大量采用了 Go 语言,尤其是在处理并发场景时,Docker 对 Go 语言的运用可谓出神入
化。本书可以帮助 Go 语言程序员亲身体验特大型项目中 Go 语言的威力,以及实战场景中
Golang 模式和功能的用法


推荐大家去买正版图书,工具书不同于小说,需要反反复复的学习


阅读地址(可以下载):


[网易云阅读]( http://yuedu.163.com/source/37aa61f7a4874857bf7a57d7a2410b7f_4 )


[CSDN]( http://download.csdn.net/detail/hzbooks/8943237 )

【转】Go文件操作大全

故城 发表了文章 • 2 个评论 • 2017 次浏览 • 2016-10-13 09:57 • 来自相关话题

> 译者博客  查看全部
					
> 译者博客  http://colobu.com/2016/10/12/g ... erral

> 原作者博客 http://www.devdungeon.com/cont ... bytes

参考
Go官方库的文件操作分散在多个包中,比如os、ioutil包,我本来想写一篇总结性的Go文件操作的文章,却发现已经有人2015年已经写了一篇这样的文章,写的非常好,所以我翻译成了中文,强烈推荐你阅读一下。

原文: Working with Files in Go, 作者: NanoDano

介绍

万物皆文件

UNIX 的一个基础设计就是"万物皆文件"(everything is a file)。我们不必知道一个文件到底映射成什么,操作系统的设备驱动抽象成文件。操作系统为设备提供了文件格式的接口。

Go语言中的reader和writer接口也类似。我们只需简单的读写字节,不必知道reader的数据来自哪里,也不必知道writer将数据发送到哪里。
你可以在/dev下查看可用的设备,有些可能需要较高的权限才能访问。

基本操作

# 创建空文件

```go
package main
import (
"log"
"os"
)
var (
newFile *os.File
err error
)
func main() {
newFile, err = os.Create("test.txt")
if err != nil {
log.Fatal(err)
}
log.Println(newFile)
newFile.Close()
}

Truncate文件


package main
import (
"log"
"os"
)
func main() {
// 裁剪一个文件到100个字节。
// 如果文件本来就少于100个字节,则文件中原始内容得以保留,剩余的字节以null字节填充。
// 如果文件本来超过100个字节,则超过的字节会被抛弃。
// 这样我们总是得到精确的100个字节的文件。
// 传入0则会清空文件。
err := os.Truncate("test.txt", 100)
if err != nil {
log.Fatal(err)
}
}

得到文件信息


package main
import (
"fmt"
"log"
"os"
)
var (
fileInfo os.FileInfo
err error
)
func main() {
// 如果文件不存在,则返回错误
fileInfo, err = os.Stat("test.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("File name:", fileInfo.Name())
fmt.Println("Size in bytes:", fileInfo.Size())
fmt.Println("Permissions:", fileInfo.Mode())
fmt.Println("Last modified:", fileInfo.ModTime())
fmt.Println("Is Directory: ", fileInfo.IsDir())
fmt.Printf("System interface type: %T\n", fileInfo.Sys())
fmt.Printf("System info: %+v\n\n", fileInfo.Sys())
}

重命名和移动


package main
import (
"log"
"os"
)
func main() {
originalPath := "test.txt"
newPath := "test2.txt"
err := os.Rename(originalPath, newPath)
if err != nil {
log.Fatal(err)
}
}

译者按: rename 和 move 原理一样


删除文件


package main
import (
"log"
"os"
)
func main() {
err := os.Remove("test.txt")
if err != nil {
log.Fatal(err)
}
}

打开和关闭文件


package main
import (
"log"
"os"
)
func main() {
// 简单地以只读的方式打开。下面的例子会介绍读写的例子。
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
file.Close()
// OpenFile提供更多的选项。
// 最后一个参数是权限模式permission mode
// 第二个是打开时的属性
file, err = os.OpenFile("test.txt", os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
file.Close()
// 下面的属性可以单独使用,也可以组合使用。
// 组合使用时可以使用 OR 操作设置 OpenFile的第二个参数,例如:
// os.O_CREATE|os.O_APPEND
// 或者 os.O_CREATE|os.O_TRUNC|os.O_WRONLY
// os.O_RDONLY // 只读
// os.O_WRONLY // 只写
// os.O_RDWR // 读写
// os.O_APPEND // 往文件中添建(Append)
// os.O_CREATE // 如果文件不存在则先创建
// os.O_TRUNC // 文件打开时裁剪文件
// os.O_EXCL // 和O_CREATE一起使用,文件不能存在
// os.O_SYNC // 以同步I/O的方式打开
}

译者按:熟悉Linux的读者应该很熟悉权限模式,通过Linux命令chmod可以更改文件的权限
https://www.linux.com/learn/understanding-linux-file-permissions


补充了原文未介绍的flag


检查文件是否存在


package main
import (
"log"
"os"
)
var (
fileInfo *os.FileInfo
err error
)
func main() {
// 文件不存在则返回error
fileInfo, err := os.Stat("test.txt")
if err != nil {
if os.IsNotExist(err) {
log.Fatal("File does not exist.")
}
}
log.Println("File does exist. File information:")
log.Println(fileInfo)
}

检查读写权限


package main
import (
"log"
"os"
)
func main() {
// 这个例子测试写权限,如果没有写权限则返回error。
// 注意文件不存在也会返回error,需要检查error的信息来获取到底是哪个错误导致。
file, err := os.OpenFile("test.txt", os.O_WRONLY, 0666)
if err != nil {
if os.IsPermission(err) {
log.Println("Error: Write permission denied.")
}
}
file.Close()
// 测试读权限
file, err = os.OpenFile("test.txt", os.O_RDONLY, 0666)
if err != nil {
if os.IsPermission(err) {
log.Println("Error: Read permission denied.")
}
}
file.Close()
}

改变权限、拥有者、时间戳


package main
import (
"log"
"os"
"time"
)
func main() {
// 使用Linux风格改变文件权限
err := os.Chmod("test.txt", 0777)
if err != nil {
log.Println(err)
}
// 改变文件所有者
err = os.Chown("test.txt", os.Getuid(), os.Getgid())
if err != nil {
log.Println(err)
}
// 改变时间戳
twoDaysFromNow := time.Now().Add(48 * time.Hour)
lastAccessTime := twoDaysFromNow
lastModifyTime := twoDaysFromNow
err = os.Chtimes("test.txt", lastAccessTime, lastModifyTime)
if err != nil {
log.Println(err)
}
}

硬链接和软链接


一个普通的文件是一个指向硬盘的inode的地方。
硬链接创建一个新的指针指向同一个地方。只有所有的链接被删除后文件才会被删除。硬链接只在相同的文件系统中才工作。你可以认为一个硬链接是一个正常的链接。


symbolic link,又叫软连接,和硬链接有点不一样,它不直接指向硬盘中的相同的地方,而是通过名字引用其它文件。他们可以指向不同的文件系统中的不同文件。并不是所有的操作系统都支持软链接。


package main
import (
"os"
"log"
"fmt"
)
func main() {
// 创建一个硬链接。
// 创建后同一个文件内容会有两个文件名,改变一个文件的内容会影响另一个。
// 删除和重命名不会影响另一个。
err := os.Link("original.txt", "original_also.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("creating sym")
// Create a symlink
err = os.Symlink("original.txt", "original_sym.txt")
if err != nil {
log.Fatal(err)
}
// Lstat返回一个文件的信息,但是当文件是一个软链接时,它返回软链接的信息,而不是引用的文件的信息。
// Symlink在Windows中不工作。
fileInfo, err := os.Lstat("original_sym.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Link info: %+v", fileInfo)
//改变软链接的拥有者不会影响原始文件。
err = os.Lchown("original_sym.txt", os.Getuid(), os.Getgid())
if err != nil {
log.Fatal(err)
}
}

读写


复制文件


package main
import (
"os"
"log"
"io"
)
func main() {
// 打开原始文件
originalFile, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
defer originalFile.Close()
// 创建新的文件作为目标文件
newFile, err := os.Create("test_copy.txt")
if err != nil {
log.Fatal(err)
}
defer newFile.Close()
// 从源中复制字节到目标文件
bytesWritten, err := io.Copy(newFile, originalFile)
if err != nil {
log.Fatal(err)
}
log.Printf("Copied %d bytes.", bytesWritten)
// 将文件内容flush到硬盘中
err = newFile.Sync()
if err != nil {
log.Fatal(err)
}
}

跳转到文件指定位置(Seek)


package main
import (
"os"
"fmt"
"log"
)
func main() {
file, _ := os.Open("test.txt")
defer file.Close()
// 偏离位置,可以是正数也可以是负数
var offset int64 = 5
// 用来计算offset的初始位置
// 0 = 文件开始位置
// 1 = 当前位置
// 2 = 文件结尾处
var whence int = 0
newPosition, err := file.Seek(offset, whence)
if err != nil {
log.Fatal(err)
}
fmt.Println("Just moved to 5:", newPosition)
// 从当前位置回退两个字节
newPosition, err = file.Seek(-2, 1)
if err != nil {
log.Fatal(err)
}
fmt.Println("Just moved back two:", newPosition)
// 使用下面的技巧得到当前的位置
currentPosition, err := file.Seek(0, 1)
fmt.Println("Current position:", currentPosition)
// 转到文件开始处
newPosition, err = file.Seek(0, 0)
if err != nil {
log.Fatal(err)
}
fmt.Println("Position after seeking 0,0:", newPosition)
}

写文件


可以使用os包写入一个打开的文件。
因为Go可执行包是静态链接的可执行文件,你import的每一个包都会增加你的可执行文件的大小。其它的包如io、`ioutil`、`bufio`提供了一些方法,但是它们不是必须的。


package main
import (
"os"
"log"
)
func main() {
// 可写方式打开文件
file, err := os.OpenFile(
"test.txt",
os.O_WRONLY|os.O_TRUNC|os.O_CREATE,
0666,
)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 写字节到文件中
byteSlice := []byte("Bytes!\n")
bytesWritten, err := file.Write(byteSlice)
if err != nil {
log.Fatal(err)
}
log.Printf("Wrote %d bytes.\n", bytesWritten)
}

快写文件


ioutil包有一个非常有用的方法WriteFile()可以处理创建/打开文件、写字节slice和关闭文件一系列的操作。如果你需要简洁快速地写字节slice到文件中,你可以使用它。


package main
import (
"io/ioutil"
"log"
)
func main() {
err := ioutil.WriteFile("test.txt", []byte("Hi\n"), 0666)
if err != nil {
log.Fatal(err)
}
}

使用缓存写


bufio包提供了带缓存功能的writer,所以你可以在写字节到硬盘前使用内存缓存。当你处理很多的数据很有用,因为它可以节省操作硬盘I/O的时间。在其它一些情况下它也很有用,比如你每次写一个字节,把它们攒在内存缓存中,然后一次写入到硬盘中,减少硬盘的磨损以及提升性能。


package main
import (
"log"
"os"
"bufio"
)
func main() {
// 打开文件,只写
file, err := os.OpenFile("test.txt", os.O_WRONLY, 0666)
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 为这个文件创建buffered writer
bufferedWriter := bufio.NewWriter(file)
// 写字节到buffer
bytesWritten, err := bufferedWriter.Write(
[]byte{65, 66, 67},
)
if err != nil {
log.Fatal(err)
}
log.Printf("Bytes written: %d\n", bytesWritten)
// 写字符串到buffer
// 也可以使用 WriteRune() 和 WriteByte()
bytesWritten, err = bufferedWriter.WriteString(
"Buffered string\n",
)
if err != nil {
log.Fatal(err)
}
log.Printf("Bytes written: %d\n", bytesWritten)
// 检查缓存中的字节数
unflushedBufferSize := bufferedWriter.Buffered()
log.Printf("Bytes buffered: %d\n", unflushedBufferSize)
// 还有多少字节可用(未使用的缓存大小)
bytesAvailable := bufferedWriter.Available()
if err != nil {
log.Fatal(err)
}
log.Printf("Available buffer: %d\n", bytesAvailable)
// 写内存buffer到硬盘
bufferedWriter.Flush()
// 丢弃还没有flush的缓存的内容,清除错误并把它的输出传给参数中的writer
// 当你想将缓存传给另外一个writer时有用
bufferedWriter.Reset(bufferedWriter)
bytesAvailable = bufferedWriter.Available()
if err != nil {
log.Fatal(err)
}
log.Printf("Available buffer: %d\n", bytesAvailable)
// 重新设置缓存的大小。
// 第一个参数是缓存应该输出到哪里,这个例子中我们使用相同的writer。
// 如果我们设置的新的大小小于第一个参数writer的缓存大小, 比如10,我们不会得到一个10字节大小的缓存,
// 而是writer的原始大小的缓存,默认是4096。
// 它的功能主要还是为了扩容。
bufferedWriter = bufio.NewWriterSize(
bufferedWriter,
8000,
)
// resize后检查缓存的大小
bytesAvailable = bufferedWriter.Available()
if err != nil {
log.Fatal(err)
}
log.Printf("Available buffer: %d\n", bytesAvailable)
}

读取最多N个字节


os.File提供了文件操作的基本功能, 而io、ioutil、bufio提供了额外的辅助函数。


package main
import (
"os"
"log"
)
func main() {
// 打开文件,只读
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
// 从文件中读取len(b)字节的文件。
// 返回0字节意味着读取到文件尾了
// 读取到文件会返回io.EOF的error
byteSlice := make([]byte, 16)
bytesRead, err := file.Read(byteSlice)
if err != nil {
log.Fatal(err)
}
log.Printf("Number of bytes read: %d\n", bytesRead)
log.Printf("Data read: %s\n", byteSlice)
}

读取正好N个字节


package main
import (
"os"
"log"
"io"
)
func main() {
// Open file for reading
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
// file.Read()可以读取一个小文件到大的byte slice中,
// 但是io.ReadFull()在文件的字节数小于byte slice字节数的时候会返回错误
byteSlice := make([]byte, 2)
numBytesRead, err := io.ReadFull(file, byteSlice)
if err != nil {
log.Fatal(err)
}
log.Printf("Number of bytes read: %d\n", numBytesRead)
log.Printf("Data read: %s\n", byteSlice)
}

读取至少N个字节


package main
import (
"os"
"log"
"io"
)
func main() {
// 打开文件,只读
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
byteSlice := make([]byte, 512)
minBytes := 8
// io.ReadAtLeast()在不能得到最小的字节的时候会返回错误,但会把已读的文件保留
numBytesRead, err := io.ReadAtLeast(file, byteSlice, minBytes)
if err != nil {
log.Fatal(err)
}
log.Printf("Number of bytes read: %d\n", numBytesRead)
log.Printf("Data read: %s\n", byteSlice)
}

读取全部字节


package main
import (
"os"
"log"
"fmt"
"io/ioutil"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
// os.File.Read(), io.ReadFull() 和
// io.ReadAtLeast() 在读取之前都需要一个固定大小的byte slice。
// 但ioutil.ReadAll()会读取reader(这个例子中是file)的每一个字节,然后把字节slice返回。
data, err := ioutil.ReadAll(file)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Data as hex: %x\n", data)
fmt.Printf("Data as string: %s\n", data)
fmt.Println("Number of bytes read:", len(data))
}

快读到内存


package main
import (
"log"
"io/ioutil"
)
func main() {
// 读取文件到byte slice中
data, err := ioutil.ReadFile("test.txt")
if err != nil {
log.Fatal(err)
}
log.Printf("Data read: %s\n", data)
}

使用缓存读


有缓存写也有缓存读。
缓存reader会把一些内容缓存在内存中。它会提供比os.File和io.Reader更多的函数,缺省的缓存大小是4096,最小缓存是16。


package main
import (
"os"
"log"
"bufio"
"fmt"
)
func main() {
// 打开文件,创建buffered reader
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
bufferedReader := bufio.NewReader(file)
// 得到字节,当前指针不变
byteSlice := make([]byte, 5)
byteSlice, err = bufferedReader.Peek(5)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Peeked at 5 bytes: %s\n", byteSlice)
// 读取,指针同时移动
numBytesRead, err := bufferedReader.Read(byteSlice)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read %d bytes: %s\n", numBytesRead, byteSlice)
// 读取一个字节, 如果读取不成功会返回Error
myByte, err := bufferedReader.ReadByte()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read 1 byte: %c\n", myByte)
// 读取到分隔符,包含分隔符,返回byte slice
dataBytes, err := bufferedReader.ReadBytes('\n')
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read bytes: %s\n", dataBytes)
// 读取到分隔符,包含分隔符,返回字符串
dataString, err := bufferedReader.ReadString('\n')
if err != nil {
log.Fatal(err)
}
fmt.Printf("Read string: %s\n", dataString)
//这个例子读取了很多行,所以test.txt应该包含多行文本才不至于出错
}

使用 scanner


Scanner是bufio包下的类型,在处理文件中以分隔符分隔的文本时很有用。
通常我们使用换行符作为分隔符将文件内容分成多行。在CSV文件中,逗号一般作为分隔符。
os.File文件可以被包装成bufio.Scanner,它就像一个缓存reader。
我们会调用Scan()方法去读取下一个分隔符,使用Text()或者Bytes()获取读取的数据。


分隔符可以不是一个简单的字节或者字符,有一个特殊的方法可以实现分隔符的功能,以及将指针移动多少,返回什么数据。
如果没有定制的SplitFunc提供,缺省的ScanLines会使用newline字符作为分隔符,其它的分隔函数还包括ScanRunes和ScanWords,皆在bufio包中。


// To define your own split function, match this fingerprint
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
// Returning (0, nil, nil) will tell the scanner
// to scan again, but with a bigger buffer because
// it wasn't enough data to reach the delimiter
下面的例子中,为一个文件创建了bufio.Scanner,并按照单词逐个读取:

package main
import (
"os"
"log"
"fmt"
"bufio"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(file)
// 缺省的分隔函数是bufio.ScanLines,我们这里使用ScanWords。
// 也可以定制一个SplitFunc类型的分隔函数
scanner.Split(bufio.ScanWords)
// scan下一个token.
success := scanner.Scan()
if success == false {
// 出现错误或者EOF是返回Error
err = scanner.Err()
if err == nil {
log.Println("Scan completed and reached EOF")
} else {
log.Fatal(err)
}
}
// 得到数据,Bytes() 或者 Text()
fmt.Println("First word found:", scanner.Text())
// 再次调用scanner.Scan()发现下一个token
}

压缩


打包(zip) 文件


// This example uses zip but standard library
// also supports tar archives
package main
import (
"archive/zip"
"log"
"os"
)
func main() {
// 创建一个打包文件
outFile, err := os.Create("test.zip")
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
// 创建zip writer
zipWriter := zip.NewWriter(outFile)
// 往打包文件中写文件。
// 这里我们使用硬编码的内容,你可以遍历一个文件夹,把文件夹下的文件以及它们的内容写入到这个打包文件中。
var filesToArchive = []struct {
Name, Body string
} {
{"test.txt", "String contents of file"},
{"test2.txt", "\x61\x62\x63\n"},
}
// 下面将要打包的内容写入到打包文件中,依次写入。
for _, file := range filesToArchive {
fileWriter, err := zipWriter.Create(file.Name)
if err != nil {
log.Fatal(err)
}
_, err = fileWriter.Write([]byte(file.Body))
if err != nil {
log.Fatal(err)
}
}
// 清理
err = zipWriter.Close()
if err != nil {
log.Fatal(err)
}
}

抽取(unzip) 文件


// This example uses zip but standard library
// also supports tar archives
package main
import (
"archive/zip"
"log"
"io"
"os"
"path/filepath"
)
func main() {
zipReader, err := zip.OpenReader("test.zip")
if err != nil {
log.Fatal(err)
}
defer zipReader.Close()
// 遍历打包文件中的每一文件/文件夹
for _, file := range zipReader.Reader.File {
// 打包文件中的文件就像普通的一个文件对象一样
zippedFile, err := file.Open()
if err != nil {
log.Fatal(err)
}
defer zippedFile.Close()
// 指定抽取的文件名。
// 你可以指定全路径名或者一个前缀,这样可以把它们放在不同的文件夹中。
// 我们这个例子使用打包文件中相同的文件名。
targetDir := "./"
extractedFilePath := filepath.Join(
targetDir,
file.Name,
)
// 抽取项目或者创建文件夹
if file.FileInfo().IsDir() {
// 创建文件夹并设置同样的权限
log.Println("Creating directory:", extractedFilePath)
os.MkdirAll(extractedFilePath, file.Mode())
} else {
//抽取正常的文件
log.Println("Extracting file:", file.Name)
outputFile, err := os.OpenFile(
extractedFilePath,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
file.Mode(),
)
if err != nil {
log.Fatal(err)
}
defer outputFile.Close()
// 通过io.Copy简洁地复制文件内容
_, err = io.Copy(outputFile, zippedFile)
if err != nil {
log.Fatal(err)
}
}
}
}

压缩文件


// 这个例子中使用gzip压缩格式,标准库还支持zlib, bz2, flate, lzw
package main
import (
"os"
"compress/gzip"
"log"
)
func main() {
outputFile, err := os.Create("test.txt.gz")
if err != nil {
log.Fatal(err)
}
gzipWriter := gzip.NewWriter(outputFile)
defer gzipWriter.Close()
// 当我们写如到gizp writer数据时,它会依次压缩数据并写入到底层的文件中。
// 我们不必关心它是如何压缩的,还是像普通的writer一样操作即可。
_, err = gzipWriter.Write([]byte("Gophers rule!\n"))
if err != nil {
log.Fatal(err)
}
log.Println("Compressed data written to file.")
}

解压缩文件


// 这个例子中使用gzip压缩格式,标准库还支持zlib, bz2, flate, lzw
package main
import (
"compress/gzip"
"log"
"io"
"os"
)
func main() {
// 打开一个gzip文件。
// 文件是一个reader,但是我们可以使用各种数据源,比如web服务器返回的gzipped内容,
// 它的内容不是一个文件,而是一个内存流
gzipFile, err := os.Open("test.txt.gz")
if err != nil {
log.Fatal(err)
}
gzipReader, err := gzip.NewReader(gzipFile)
if err != nil {
log.Fatal(err)
}
defer gzipReader.Close()
// 解压缩到一个writer,它是一个file writer
outfileWriter, err := os.Create("unzipped.txt")
if err != nil {
log.Fatal(err)
}
defer outfileWriter.Close()
// 复制内容
_, err = io.Copy(outfileWriter, gzipReader)
if err != nil {
log.Fatal(err)
}
}

其它



临时文件和目录


ioutil提供了两个函数: TempDir() 和 TempFile()。
使用完毕后,调用者负责删除这些临时文件和文件夹。
有一点好处就是当你传递一个空字符串作为文件夹名的时候,它会在操作系统的临时文件夹中创建这些项目(/tmp on Linux)。
os.TempDir()返回当前操作系统的临时文件夹。


package main
import (
"os"
"io/ioutil"
"log"
"fmt"
)
func main() {
// 在系统临时文件夹中创建一个临时文件夹
tempDirPath, err := ioutil.TempDir("", "myTempDir")
if err != nil {
log.Fatal(err)
}
fmt.Println("Temp dir created:", tempDirPath)
// 在临时文件夹中创建临时文件
tempFile, err := ioutil.TempFile(tempDirPath, "myTempFile.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println("Temp file created:", tempFile.Name())
// ... 做一些操作 ...
// 关闭文件
err = tempFile.Close()
if err != nil {
log.Fatal(err)
}
// 删除我们创建的资源
err = os.Remove(tempFile.Name())
if err != nil {
log.Fatal(err)
}
err = os.Remove(tempDirPath)
if err != nil {
log.Fatal(err)
}
}

通过HTTP下载文件


package main
import (
"os"
"io"
"log"
"net/http"
)
func main() {
newFile, err := os.Create("devdungeon.html")
if err != nil {
log.Fatal(err)
}
defer newFile.Close()
url := "http://www.devdungeon.com/archive"
response, err := http.Get(url)
defer response.Body.Close()
// 将HTTP response Body中的内容写入到文件
// Body满足reader接口,因此我们可以使用ioutil.Copy
numBytesWritten, err := io.Copy(newFile, response.Body)
if err != nil {
log.Fatal(err)
}
log.Printf("Downloaded %d byte file.\n", numBytesWritten)
}

哈希和摘要


package main
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"log"
"fmt"
"io/ioutil"
)
func main() {
// 得到文件内容
data, err := ioutil.ReadFile("test.txt")
if err != nil {
log.Fatal(err)
}
// 计算Hash
fmt.Printf("Md5: %x\n\n", md5.Sum(data))
fmt.Printf("Sha1: %x\n\n", sha1.Sum(data))
fmt.Printf("Sha256: %x\n\n", sha256.Sum256(data))
fmt.Printf("Sha512: %x\n\n", sha512.Sum512(data))
}

上面的例子复制整个文件内容到内存中,传递给hash函数。
另一个方式是创建一个hash writer, 使用Write、WriteString、Copy将数据传给它。
下面的例子使用 md5 hash,但你可以使用其它的Writer。


```go
package main
import (
"crypto/md5"
"log"
"fmt"
"io"
"os"
)
func main() {
file, err := os.Open("test.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
//创建一个新的hasher,满足writer接口
hasher := md5.New()
_, err = io.Copy(hasher, file)
if err != nil {
log.Fatal(err)
}
// 计算hash并打印结果。
// 传递 nil 作为参数,因为我们不通参数传递数据,而是通过writer接口。
sum := hasher.Sum(nil)
fmt.Printf("Md5 checksum: %x\n", sum)
}

10.13 每日早报

astaxie 发表了文章 • 0 个评论 • 450 次浏览 • 2016-10-13 08:16 • 来自相关话题

10.13 每日早报

新闻:

1.IMAX将于2016年底前在欧洲开设首个虚拟现实体验中心

2.饿了么、阿里旅行和51社保等企业接入阿里钉钉平台

3.网易有道与考神团队成立教育合资公司,将投入5亿元... 查看全部

10.13 每日早报


新闻:


1.IMAX将于2016年底前在欧洲开设首个虚拟现实体验中心


2.饿了么、阿里旅行和51社保等企业接入阿里钉钉平台


3.网易有道与考神团队成立教育合资公司,将投入5亿元孵化教育工作室


4.百度成立百度资本,投资泛互联网领域中后期项目,一期基金200亿


5.英特尔推出自主品牌工业无人机Falcon 8+,聚焦工业场合应用


6.亚马逊计划开设便利店和路边取货点,用户可在路边区域提取商品


7.阿里游戏、金立游戏等多家手游平台提醒开发者提供总局出版批文


8.德国航空航天中心完成高清版3D世界地图,可免费用于科研项目


资源:


2016年中国视频网站付费用户典型案例研究报告
http://report.iresearch.cn/report/201610/2653.shtml


中国的移动互联网Top 2000 APP秋季盘点
http://www.questmobile.com.cn/blog/blog_61.html


注:上述内容来源于互联网,由EGO整理

Python 程序员的 Golang 学习指南(II): 开发环境搭建

Cloudinsight 发表了文章 • 0 个评论 • 1757 次浏览 • 2016-10-12 15:44 • 来自相关话题

Authors: startover

Authors: startover





上一篇文章我们已经对 Golang 有了初步的了解,这篇主要介绍如何在 Ubuntu 14.04 上搭建 Golang 开发环境。


安装 Golang


这里就按照官方文档进行安装即可,如下:



  • 下载并解压安装包到指定目录


$ wget https://storage.googleapis.com ... ar.gz
$ tar -C /usr/local -xzf go1.6.3.linux-amd64.tar.gz


  • 设置 PATH


$ echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.bashrc
$ source ~/.bashrc


  • 验证安装


$ go version
go version go1.6.3 linux/amd64

环境变量设置


$ echo "export GOROOT=/usr/local/go" >> ~/.bashrc
$ echo "export GOPATH=$HOME/go" >> ~/.bashrc
$ source ~/.bashrc

其中,GOROOT 为 Golang 的安装目录,只有当 Golang 安装到除 /usr/local 之外的路径时需要设置,反之则不用设置,GOPATH 是 Golang 的开发目录,详细可参考官方文档


开发工具


工欲善其事,必先利其器,作为一名伪 VIMer,这里主要介绍下如何在 Vim 下配置 Golang 开发环境。


由于之前一直使用 k-vim 作为 Python 开发环境,而 k-vim 已经集成了当前使用最为广泛的用于搭建 Golang 开发环境的 vim 插件 vim-go,只是默认没有开启,需要我们手动进行相关设置。


k-vim 中开启 Golang 语言的支持,非常简单,如下:



  • 修改 ~/.vimrc.bundles(开启 golang 支持,并修改 vim-go 的默认配置,增加快捷键配置等)。


let g:bundle_groups=['python', 'javascript', 'markdown', 'html', 'css', 'tmux', 'beta', 'json', 'golang']

" vimgo {{{
let g:go_highlight_functions = 1
let g:go_highlight_methods = 1
let g:go_highlight_structs = 1
let g:go_highlight_operators = 1
let g:go_highlight_build_constraints = 1

let g:go_fmt_fail_silently = 1
let g:go_fmt_command = "goimports"
let g:syntastic_go_checkers = ['golint', 'govet', 'errcheck']

" vim-go custom mappings
au FileType go nmap <Leader>s <Plug>(go-implements)
au FileType go nmap <Leader>i <Plug>(go-info)
au FileType go nmap <Leader>gd <Plug>(go-doc)
au FileType go nmap <Leader>gv <Plug>(go-doc-vertical)
au FileType go nmap <leader>r <Plug>(go-run)
au FileType go nmap <leader>b <Plug>(go-build)
au FileType go nmap <leader>t <Plug>(go-test)
au FileType go nmap <leader>c <Plug>(go-coverage)
au FileType go nmap <Leader>ds <Plug>(go-def-split)
au FileType go nmap <Leader>dv <Plug>(go-def-vertical)
au FileType go nmap <Leader>dt <Plug>(go-def-tab)
au FileType go nmap <Leader>e <Plug>(go-rename)
au FileType go nnoremap <leader>gr :GoRun %<CR>
" }}}



  • 在 Vim 内执行 :PlugInstall,安装 vim-go




  • 在 Vim 内执行 :GoInstallBinaries,下载并安装 vim-go 依赖的二进制工具,goimportsgolint 等。



  • 安装 gotags,使 tagbar 配置生效。


$ go get -u github.com/jstemmer/gotags

我们来看一下最终效果:


Image of Golang Environment in Vim


编写第一个程序


进入工作目录,新建文件 hello.go,如下:


$ cd $GOPATH
$ vim hello.go
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

运行程序:


$ go run hello.go
Hello, World!



本文章为 Cloudinsight 技术团队工程师原创,更多技术文章可访问 Cloudinsight 技术博客Cloudinsight 为可视化系统监控工具,涵盖 Windows、Linux 操作系统,用 Golang 开发的 Cloudinsight Agent 正式开源了,欢迎 fork,Github:https://github.com/cloudinsight/cloudinsight-agent


golang-for-pythonistas 系列持续更新中,欢迎关注~

Python 程序员的 Golang 学习指南(I): Go 之初体验

Cloudinsight 发表了文章 • 3 个评论 • 1739 次浏览 • 2016-10-12 15:27 • 来自相关话题

Authors: startover

Authors: startover





Go 语言简介


Go,又称 golang,是 Google 开发的一种静态强类型,编译型,并发型,并具有垃圾回收功能的编程语言。


Go 语言于2009年11月正式宣布推出,自2012年发布1.0,最新稳定版1.7。目前,Go的相关工具和生态已逐渐趋于完善,也不乏重量级项目,如 Docker, Kubernetes, Etcd, InfluxDB 等。


Go 语言能解决什么样的问题


同绝大多数通用型编程语言相比,Go 语言更多的是为了解决我们在构建大型服务器软件过程中所遇到的软件工程方面的问题而设计的。乍看上去,这么讲可能会让人感觉 Go 非常无趣且工业化,但实际上,在设计过程中就着重于清晰和简洁,以及较高的可组合性,最后得到的反而会是一门使用起来效率高而且很有趣的编程语言,很多程序员都会发现,它有极强的表达力而且功能非常强大。


总结为以下几点:



  • 清晰的依赖关系

  • 清晰的语法

  • 清晰的语义

  • 偏向组合而不是继承

  • 提供简单的编程模型(垃圾回收、并发)

  • 强大的内置工具(gofmt、godoc、gofix等)


建议有兴趣的同学看看 Go在谷歌:以软件工程为目的的语言设计


Go 语言相对 Python 有哪些优势


这里引用一段知乎上某大牛的回答,如下:




  • 部署简单。Go 编译生成的是一个静态可执行文件,除了 glibc 外没有其他外部依赖。这让部署变得异常方便:目标机器上只需要一个基础的系统和必要的管理、监控工具,完全不需要操心应用所需的各种包、库的依赖关系,大大减轻了维护的负担。这和 Python 有着巨大的区别。由于历史的原因,Python 的部署工具生态相当混乱【比如 setuptools, distutils, pip, buildout 的不同适用场合以及兼容性问题】。官方 PyPI 源又经常出问题,需要搭建私有镜像,而维护这个镜像又要花费不少时间和精力。




  • 并发性好。Goroutine 和 channel 使得编写高并发的服务端软件变得相当容易,很多情况下完全不需要考虑锁机制以及由此带来的各种问题。单个 Go 应用也能有效的利用多个 CPU 核,并行执行的性能好。这和 Python 也是天壤之比。多线程和多进程的服务端程序编写起来并不简单,而且由于全局锁 GIL 的原因,多线程的 Python 程序并不能有效利用多核,只能用多进程的方式部署;如果用标准库里的 multiprocessing 包又会对监控和管理造成不少的挑战【我们用的 supervisor 管理进程,对 fork 支持不好】。部署 Python 应用的时候通常是每个 CPU 核部署一个应用,这会造成不少资源的浪费,比如假设某个 Python 应用启动后需要占用 100MB 内存,而服务器有 32 个 CPU 核,那么留一个核给系统、运行 31 个应用副本就要浪费 3GB 的内存资源。




  • 良好的语言设计。从学术的角度讲 Go 语言其实非常平庸,不支持许多高级的语言特性;但从工程的角度讲,Go 的设计是非常优秀的:规范足够简单灵活,有其他语言基础的程序员都能迅速上手。更重要的是 Go 自带完善的工具链,大大提高了团队协作的一致性。比如 gofmt 自动排版 Go 代码,很大程度上杜绝了不同人写的代码排版风格不一致的问题。把编辑器配置成在编辑存档的时候自动运行 gofmt,这样在编写代码的时候可以随意摆放位置,存档的时候自动变成正确排版的代码。此外还有 gofix, govet 等非常有用的工具。



  • 执行性能好。虽然不如 C 和 Java,但通常比原生 Python 应用还是高一个数量级的,适合编写一些瓶颈业务。内存占用也非常省。


从个人对 Golang 的初步使用来说,体验还是相当不错的,但是也有下面几点需要注意:




  • 驼峰式命名风格(依据首字母大小写来决定其是否能被其他包引用),但我更喜欢 Python 的小写字母加下划线命名风格。




  • 没有好用的包管理器,Golang 官方也没有推荐最佳的包管理方案,目前公认的比较好用的有 Godeps, Govendor 及 Glide,而 Python 的包管理器 pip 已形成自己的一套标准。




  • 多行字符串的变量声明需要用反引号(`),Python 里是三个双引号("""),参考http://stackoverflow.com/questions/7933460/how-do-you-write-multiline-strings-in-go




  • Golang 中的类型匹配是很严格的,不同的类型之间通常需要手动转换,所以在字符串拼接时往往需要对整型进行显式转换,如 fmt.Println("num: " + strconv.Itoa(1))



  • Golang 语言语法里的语法糖并不多,如在 Python 中很流行的 map, reduce, range 等,在 Golang 里都没有得到支持。


另外,推荐阅读 Golang 新手开发者要注意的陷阱和常见错误


学习资料推荐


建议先把 Go 的官方文档过一遍,主要有以下几项:



官方文档看完后,基本也算入门了,这时候可以看看 Go 的示例代码,或者去 Project Euler 刷刷题。


当然也可以去知乎看看大牛们都是如何学习的,链接 https://www.zhihu.com/question/23486344


总结


虽然 Go 有很多被诟病的地方,比如 GC 和对错误的处理方式,但没有任何语言是完美的,从实用角度来讲,Go 有着不输于 Python 的开发效率,完善的第三方工具,以及强大的社区支持,这些就足够了。


相关链接:

https://golang.org/doc/

https://talks.golang.org/2012/splash.article

https://www.zhihu.com/question/21409296

https://www.zhihu.com/question/23486344

http://stackoverflow.com/questions/7933460/how-do-you-write-multiline-strings-in-go

http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/

http://www.oschina.net/translate/go-at-google-language-design-in-the-service-of-software-engineering




本文章为 Cloudinsight 技术团队工程师原创,更多技术文章可访问 Cloudinsight 技术博客Cloudinsight 为可视化系统监控工具,涵盖 Windows、Linux 操作系统,用 Golang 开发的 Cloudinsight Agent 正式开源了,欢迎 fork,Github:https://github.com/cloudinsight/cloudinsight-agent


golang-for-pythonistas 系列持续更新中,欢迎关注~

Go语言并发模型:使用 select

ming 发表了文章 • 0 个评论 • 1240 次浏览 • 2016-10-12 12:30 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

... 查看全部


此文章已得到翻译者授权转载,点击查看原文



简介


作为一种现代语言,go语言实现了对并发的原生支持。上几期文章中,我们对goroutine 和 channel进行了详细的讲解。
但是要实现对 channel 的控制,从语言层面上来说,select 语句是必不可少的部分。本文中,我们就 select 语句的
行为和使用方法进行深入讨论。


阅读建议


本文中的内容是 Go语言并发模型的一篇,但是与上几期关系不是特别密切,可以独立阅读。本文的内容源自于
go language specifications
Rob Pike 在2012年进行的一场名为"concurrency"
的演讲。如果有时间的话,建议在 YouTube 上看一下他本人的演讲。


select 语句的行为


为了便于理解,我们首先给出一个代码片段:


// https://talks.golang.org/2012/concurrency.slide#32
select {
case v1 := <-c1:
fmt.Printf("received %v from c1\n", v1)
case v2 := <-c2:
fmt.Printf("received %v from c2\n", v1)
case c3 <- 23:
fmt.Printf("sent %v to c3\n", 23)
default:
fmt.Printf("no one was ready to communicate\n")
}

上面这段代码中,select 语句有四个 case 子语句,前两个是 receive 操作,第三个是 send 操作,最后一个是默认操作。
代码执行到 select 时,case 语句会按照源代码的顺序被评估,且只评估一次,评估的结果会出现下面这几种情况:



  1. 除 default 外,如果只有一个 case 语句评估通过,那么就执行这个case里的语句;

  2. 除 default 外,如果有多个 case 语句评估通过,那么通过伪随机的方式随机选一个;

  3. 如果 default 外的 case 语句都没有通过评估,那么执行 default 里的语句;

  4. 如果没有 default,那么 代码块会被阻塞,指导有一个 case 通过评估;否则一直阻塞


如果 case 语句中 的 receive 操作的对象是 nil channel,那么也会阻塞,下面我们看一个更全面、用法也更高级的例子:


// https://golang.org/ref/spec#Select_statements
var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
print("received ", i1, " from c1\n")
case c2 <- i2:
print("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
print("received ", i3, " from c3\n")
} else {
print("c3 is closed\n")
}
case a[f()] = <-c4:
// same as:
// case t := <-c4
// a[f()] = t
default:
print("no communication\n")
}

for { // 向 channel c 发送随机 bit 串
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}

select {} // 永久阻塞

注意:与 C/C++ 等传统编程语言不同,go语言的 case 语句不需要 break 关键字去跳出 select。


select 的使用


为请求设置超时时间


在 golang 1.7 之前, http 包并没有引入 context 支持,通过 http.Client 向一个坏掉的服务发送请求会导致响应缓慢。
类似的场景下,我们可以使用 select 控制服务响应时间,下面是一个简单的demo:


func main() {
c := boring("Joe")
timeout := time.After(5 * time.Second)
for {
select {
case s := <-c:
fmt.Println(s)
case <-timeout:
fmt.Println("You talk too much.")
return
}
}
}

done channel


上几期的文章中,我们均讨论过 done channel,它可以用于保证流水线上每个阶段goroutine 的退出。在 golang.org/x/net 包中,
done channel 被广泛应用。这里我们看一眼 golang.org/x/net/context/ctxhttp 中 Do 方法的实现:


// https://github.com/golang/net/ ... tp.go

// Do sends an HTTP request with the provided http.Client and returns
// an HTTP response.
//
// If the client is nil, http.DefaultClient is used.
//
// The provided ctx must be non-nil. If it is canceled or times out,
// ctx.Err() will be returned.
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req.WithContext(ctx))
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
if err != nil {
select {
case <-ctx.Done():
err = ctx.Err()
default:
}
}
return resp, err
}

quit channel


在很多场景下,quit channel 和 done channel 是一个概念。在并发程序中,通常 main routine 将
任务分给其它 go routine 去完成,而自身只是起到调度作用。这种情况下,main 函数无法知道 其它goroutine
任务是否完成,此时我们需要 quit channel;对于更细粒度的控制,比如完成多少,还是需要 done channel (参考WaitGroup)。
下面是 quit channel 的一个例子,首先是 main routine:


// 创建 quit channel
quit := make(chan string)
// 启动生产者 goroutine
c := boring("Joe", quit)
// 从生产者 channel 读取结果
for i := rand.Intn(10); i >= 0; i-- { fmt.Println(<-c) }
// 通过 quit channel 通知生产者停止生产
quit <- "Bye!"
fmt.Printf("Joe says: %q\n", <-quit)

我们再看 生产者 go routine 中与 quit channel 相关的部分:


select {
case c <- fmt.Sprintf("%s: %d", msg, i):
// do nothing
case <-quit:
cleanup()
quit <- "See you!"
return
}

Google Search (延伸阅读)


Google Search 是一个很经典的例子,由于代码较多,有兴趣的童鞋查看 Rob Pike 的 ppt
更高阶的并发方式可以阅读 Sameer Ajmani 的 ppt Advanced Go Concurrency Patterns


并发相关的主题就先到这里,下一期文章中,我们会讨论go语言测试工具链中的单元测试。


相关链接:



  1. Rob Pike演讲:concurrency

  2. [language specification: select statement](https://golang.org/ref/spec#Select_statements “select")


扫码关注微信公众号“深入Go语言”


在这里

Go语言并发模型:使用 context

ming 发表了文章 • 1 个评论 • 750 次浏览 • 2016-10-12 12:28 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。
请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。
用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、
验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的
goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。


在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与
请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
你可以通过 go get golang.org/x/net/context 命令获取这个包。本文要讲的就是如果使用
这个包,同时也会提供一个完整的例子。


阅读建议


本文内容涉及到了 done channel,如果你不了解这个概念,那么请先阅读 "Go语言并发模型:像Unix Pipe那样使用channel"


由于访问 golang.org/x/net/context 需要梯子,你可以访问它在 github 上的 mirror
如果要下载本文中的代码,可以查看文章末尾的“相关链接”环节。


package context


context 包的核心是 struct Context,声明如下:


// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}

// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error

// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)

// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

注意: 这里我们对描述进行了简化,更详细的描述查看 godoc:context


Done 方法返回一个 channel,这个 channel 对于以 Context 方式运行的函数而言,是一个取消信号。
当这个 channel 关闭时,上面提到的这些函数应该终止手头的工作并立即返回。 之后,Err 方法会返回一个错误,
告知为什么 Context 被取消。关于 Done channel 的更多细节查看上一篇文章 "Go语言并发模型:像Unix Pipe那样使用channel"


一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。背后的原因是一致的:接收取消信号的函数
和发送信号的函数通常不是一个。 一个典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。
作为一个折中,WithCancel 函数 (后面会细说) 提供了一种取消新的 Context 的方法。


Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,
对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。


Deadline 方法允许函数确定它们是否应该开始工作。如果剩下的时间太少,也许这些函数就不值得启动。
代码中,我们也可以使用 Deadline 对象为 I/O 操作设置截止时间。


Value 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。


继承 context


context 包提供了一些函数,协助用户从现有的 Context 对象创建新的 Context 对象。
这些 Context 对象形成一棵树:当一个 Context 对象被取消时,继承自它的所有 Context 都会被取消。


Background 是所有 Context 对象树的根,它不能被取消。它的声明如下:


// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level `Context` for incoming requests.
func Background() Context

WithCancelWithTimeout 函数 会返回继承的 Context 对象, 这些对象可以比它们的父 Context 更早地取消。


当请求处理函数返回时,与该请求关联的 Context 会被取消。 当使用多个副本发送请求时,可以使用 WithCancel
取消多余的请求。 WithTimeout 在设置对后端服务器请求截止时间时非常有用。 下面是这三个函数的声明:


// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:


// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

当然,想要知道 Context 包是如何工作的,最好的方法是看一个栗子。


一个栗子:Google Web Search


我们的例子是一个 HTTP 服务,它能够将类似于 /search?q=golang&timeout=1s 的请求 转发给
Google Web Search API,然后渲染返回的结果。
timeout 参数用来告诉 server 时间到时取消请求。


这个例子的代码存放在三个包里:



  1. server:它提供 main 函数和 处理 /search 的 http handler

  2. userip:它能够从 请求解析用户的IP,并将请求绑定到一个 Context 对象。

  3. google:它包含了 Search 函数,用来向 Google 发送请求。


深入 server 程序


server 程序处理类似于 /search?q=golang 的请求,
返回 Google API 的搜索结果。它将 handleSearch 函数注册到 /search 路由。处理函数创建一个 Context ctx,并对其进行初始化,
以保证 Context 取消时,处理函数返回。如果请求的 URL 参数中包含 timeout,那么当 timeout 到期时, Context 会被自动取消。
handleSearch 的代码如下:


func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx is the `Context` for this handler. Calling cancel closes the
// ctx.Done channel, which is the cancellation signal for requests
// started by this handler.
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a `Context` that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.

处理函数 (handleSearch) 将query 参数从请求中解析出来,然后通过 userip 包将client IP解析出来。这里 Client IP 在
后端发送请求时要用到,所以 handleSearch 函数将它 attach 到 Context 对象 ctx 上。代码如下:


// Check the search query.
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}

// Store the user IP in ctx for use by code in other packages.
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)

处理函数带着 Context 对象 ctxquery 调用 google.Search,代码如下:


// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)

如果搜索成功,处理函数会渲染搜索结果,代码如下:


if err := resultsTemplate.Execute(w, struct {
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err != nil {
log.Print(err)
return
}

深入 userip 包


userip 包提供了两个功能:1. 从请求解析出
Client IP;2. 将 Client IP 关联到一个 Context 对象。一个 Context 对象提供一个 key-value 映射,key 和 value
的类型都是 interface{},但是 key 必须满足等价性(可以比较),value 必须是线程安全的。类似于 userip 的包
隐藏了映射的细节,提供的是对特定 Context 类型值得强类型访问。


为了避免 key 冲突,userip 定义了一个非输出类型 key,并使用该类型的值作为 Context 的key。代码如下:


// 为了避免与其他包中的 `Context` key 冲突
// 这里不输出 key 类型 (首字母小写)
type key int

// userIPKey 是 user IP 的 `Context` key
// 它的值是随意写的。如果这个包中定义了其他
// `Context` key,这些 key 必须不同
const userIPKey key = 0

函数 FromRequest 用来从一个 http.Request 对象中解析出 userIP:


func FromRequest(req *http.Request) (net.IP, error) {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}

函数 NewContext 返回一个新的 Context 对象,它携带者 userIP:


func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}

函数 FromContext 从一个 Context 对象中解析 userIP:


func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value returns nil if ctx has no value for the key;
// the net.IP type assertion returns ok=false for nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}

深入 google 包


函数 google.Search 想 Google Web Search API 发送一个 HTTP 请求,并解析返回的 JSON 数据。
该函数接收一个 Context 对象 ctx 作为第一参数,在请求还没有返回时,一旦 ctx.Done 关闭,该函数也会立即返回。


Google Web Search API 请求包含 query 关键字和 user IP 两个参数。具体实现如下:


func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request.
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)

// If ctx is carrying the user IP address, forward it to the server.
// Google APIs use the user IP to distinguish server-initiated requests
// from end-user requests.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()

函数 Search 使用一个辅助函数 httpDo 发送 HTTP 请求,并在 ctx.Done 关闭时取消请求 (如果还在处理请求或返回)。
函数 Search 传递给 httpDo 一个闭包处理 HTTP 结果。下面是具体实现:


var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()

// Parse the JSON search result.
// https://developers.google.com/ ... fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo waits for the closure we provided to return, so it's safe to
// read results here.
return results, err

函数 httpDo 在一个新的 goroutine 中发送 HTTP 请求和处理结果。如果 ctx.Done 已经关闭,
而处理请求的 goroutine 还存在,那么取消请求。下面是具体实现:


func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
tr := &http.Transport{}
client := &http.Client{Transport: tr}
c := make(chan error, 1)
go func() { c <- f(client.Do(req)) }()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}

在自己的代码中使用 Context


许多服务器框架都提供了管理请求作用域数据的包和类型。我们可以定义一个 Context 接口的实现,
将已有代码和期望 Context 参数的代码粘合起来。


举个栗子,Gorilla 框架的 github.com/gorilla/context
允许处理函数 (handlers) 将数据和请求结合起来,他通过 HTTP 请求 到 key-value对 的映射来实现。
gorilla.go 中,我们提供了一个 Context 的具体实现,
这个实现的 Value 方法返回的值已经与 gorilla 包中特定的 HTTP 请求关联起来。


还有一些包实现了类似于 Context 的取消机制。比如 Tomb 中有一个 Kill 方法,
该方法通过关闭 名为Dying 的 channel 发送取消信号。Tomb 也提供了等待 goroutine 退出的方法,类似于 sync.WaitGroup
tomb.go 中,我们提供了一个 Context 的实现,当它的父 Context 被取消
或 一个 Tomb 对象被 kill 时,该 Context 对象也会被取消。


结论


在 Google, 我们要求 Go 程序员把 Context 作为第一个参数传递给 入口请求和出口请求链路上的每一个函数。
这样一种机制一方面保证了多个团队开发的 Go 项目能够良好地协作,另一方面它是一种简单的超时和取消机制,
保证了临界区数据 (比如安全凭证) 在不同的 Go 项目中顺利传递。


如果你要在 Context 之上构建服务器框架,需要一个自己的 Context 实现,在框架与期望 Context 参数的代码之间建立一座桥梁。
当然,Client 库也需要接收一个 Context 对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context
分享代码、创建可扩展的服务都会非常方便。


原作者:Sameer Ajmani 翻译:Oscar


下期预告:Go语言并发模型:使用 select (原文链接)。


相关链接:



  1. 原文链接

  2. [代码位置](https://blog.golang.org/context/ “context")

  3. 代码位置(mirror)

  4. mirror of package net

  5. Google Web Search API


扫码关注微信公众号“深入Go语言”


在这里

Go语言并发模型:以并行处理MD5为例

ming 发表了文章 • 1 个评论 • 569 次浏览 • 2016-10-12 12:27 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

<... 查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


Go语言的并发原语允许开发者以类似于 Unix Pipe 的方式构建数据流水线 (data pipelines),数据流水线能够高效地利用 I/O和多核 CPU 的优势。


本文要讲的就是一些使用流水线的一些例子,流水线的错误处理也是本文的重点。


阅读建议


本文是"Go语言并发模型:像Unix Pipe那样使用channel"
一文的下半部分,但重点在于实践。如果你对 channel 已经比较熟悉,则可以独立阅读。
如果你对 channel 和 go 两个关键字不太熟悉,建议先阅读上半部分。


本文所使用的例子是批量计算文件的MD5值,实现了 linux 下的 md5sum 命令。
我们首先会讲到 md5sum 的单线程版本,逐步深入到并发的初级和高级版本。


本文中绝大多数讲解都是基于代码进行的。在文章末尾"相关链接"中可以下载三个版本的 md5sum 的实现。


单线程版的 md5sum


MD5 是一种广泛用于文件校验的 hash 算法。Linux 下的 md5sum 命令会打印一组文件的 md5值。它的使用方式如下:


% md5sum *.go
c33237079343a4d567a2a29df0b8e46e bounded.go
a7e3771f2ed58d4b34a73566d93ce63a parallel.go
1dc687202696d650594aaac56d579179 serial.go

我们的示例程序类似于 md5sum,但是它接收文件夹作为参数,并打印出每个文件的 md5值,打印结果按照路径排序。
下面这个例子是 打印当前目录下所有文件的 md5 值:


% go run serial.go .
c33237079343a4d567a2a29df0b8e46e bounded.go
a7e3771f2ed58d4b34a73566d93ce63a parallel.go
1dc687202696d650594aaac56d579179 serial.go

程序的 main 函数调用辅助函数 MD5All,它会返回路径名称到md5值的一个映射。main 函数中对结果进行排序以后,打印出来:


func main() {
// 计算特定目录下所有文件的 md5值,
// 然后按照路径名顺序打印结果
m, err := MD5All(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
var paths []string
for path := range m {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
fmt.Printf("%x %s\n", m[path], path)
}
}

本文中,函数 MD5All 是讨论的焦点。在 serial.go的实现中,我们没有使用并发,而是逐个读取和计算 filepath.Walk 生成的目录和文件。代码如下:


// MD5All 读取 root 目录下的所有文件,返回一个map
// 该 map 存储了 文件路径到文件内容 md5值的映射
// 如果 Walk 执行失败,或者 ioutil.ReadFile 读取失败,
// MD5All 都会返回错误
func MD5All(root string) (map[string][md5.Size]byte, error) {
m := make(map[string][md5.Size]byte)
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
m[path] = md5.Sum(data)
return nil
})
if err != nil {
return nil, err
}
return m, nil
}

上面的代码中,filepath.Walk 接收两个参数,文件路径和函数指针。
只要是函数签名和返回值 满足 func(string, os.FileInfo, error) error,均可以作为第二参数传递给 filepath.Walk。


点击 serial.go 下载单线程版本的 md5sum。


并发版的 md5sum


点击 parallel.go 下载并发版 md5sum 的代码。


在这个版本的实现中,我们把 MD5All 切割成两个阶段的流水线。
第一阶段是 sumFiles,它遍历文件树,每个文件都在一个新的 goroutine 里计算md5值,然后将结果发送到一个result 类型的channel里。
result 类型的定义如下:


type result struct {
path string
sum [md5.Size]byte
err error
}

sumFiles 返回两个 channel,一个用于接收 md5计算的结果,一个用于接收 filepath.Walk 产生的错误。
Walk 函数为每一个文件创建一个 goroutine,然后检查 done channel。如果 done channel 被关闭,walk 函数立即停止执行。代码示例如下:


func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) {
// 对于每一个普通文件,启动一个 gorotuine 计算文件 md5 值,
// 然后 将结果发送到 c。
// walk 的错误结果发送到 errc。
c := make(chan result)
errc := make(chan error, 1)
go func() {
var wg sync.WaitGroup
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
wg.Add(1)
go func() {
data, err := ioutil.ReadFile(path)
select {
case c <- result{path, md5.Sum(data), err}:
case <-done:
}
wg.Done()
}()
// done channel 关闭时,终止 walk 函数
select {
case <-done:
return errors.New("walk canceled")
default:
return nil
}
})
// Walk 函数已经返回,所以 所有对 wg.Add 的调用都会结束
// 启动一个 goroutine, 它会在所有发送都结束时,关闭 c。
go func() {
wg.Wait()
close(c)
}()
// 这里不需要 select 语句,应为 errc 是缓冲管道
errc <- err
}()
return c, errc
}

MD5All 从 c 接收 md5值。 MD5All 遇到错误时会提前返回,通过 defer 语句关闭 done channel:


func MD5All(root string) (map[string][md5.Size]byte, error) {
// MD5All 在函数返回时关闭 done channel
// 在从 c 和 errc 接收数据前,也可能关闭
done := make(chan struct{})
defer close(done)

c, errc := sumFiles(done, root)

m := make(map[string][md5.Size]byte)
for r := range c {
if r.err != nil {
return nil, r.err
}
m[r.path] = r.sum
}
if err := <-errc; err != nil {
return nil, err
}
return m, nil
}

限制并发量


并发版 MD5All (parallel.go) 的实现中,
我们为每个文件创建了一个 goroutine。如果一个目录中包含很多大文件,可能出现OOM。


我们对并发读取的文件数目稍作限制,进而限制内存的分配。点击 bounded.go
查看限制并发版本的 md5sum。 为了实现限制的目的,我们创建固定数量的 goroutine 用于读取文件。
这里的流水线包含三个阶段:遍历文件和目录、读取并计算md5值、搜集和整合计算结果。


第一阶段时 walkFiles,它生成一个目录下每个普通文件的路径。代码如下:


func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
paths := make(chan string)
errc := make(chan error, 1)
go func() {
// Walk 函数返回时,关闭 channel paths
defer close(paths)
// 这里不需要select,因为 errc 是缓冲 channel
errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
select {
case paths <- path:
case <-done:
return errors.New("walk canceled")
}
return nil
})
}()
return paths, errc
}

第二阶段创建固定个数的goroutine digester,每个 digester 从 paths channel 读取文件名,并将结果发送给 c。代码如下:


func digester(done <-chan struct{}, paths <-chan string, c chan<- result) {
for path := range paths {
data, err := ioutil.ReadFile(path)
select {
case c <- result{path, md5.Sum(data), err}:
case <-done:
return
}
}
}

不像前面的例子,这里 digester 没有关闭输出 channel c,因为 多个 digester 在共享这个channel。
关闭操作放到 MD5All 中实现,当所有 digester 运行结束时,MD5All 关闭这个channel。代码如下:


    // 启动固定数量的 goroutine 处理文件
c := make(chan result)
var wg sync.WaitGroup
const numDigesters = 20
wg.Add(numDigesters)
for i := 0; i < numDigesters; i++ {
go func() {
digester(done, paths, c)
wg.Done()
}()
}
go func() {
wg.Wait()
close(c)
}()

我们可以让每个 digester 创建和返回自己的输出 channel。如果这样做,我们还需要额外的 goroutine 去合并结果。


第三阶段从 channel c 接收结果,并从 channel errc 读取错误信息并执行检查。
检查操作不能在 c 读取结束之前完成,因为 walkFiles 函数可能会被阻塞而无法向下游阶段发送数据。 代码如下:


// ... 省略部分代码 ...
m := make(map[string][md5.Size]byte)
for r := range c {
if r.err != nil {
return nil, r.err
}
m[r.path] = r.sum
}
// Check whether the Walk failed.
if err := <-errc; err != nil {
return nil, err
}
return m, nil
}

关于Go语言并发模型,使用 Go内置的 channel 类型和 go 关键字实现高并发和并发控制的主题就先到这里。
在最近发布的 go 1.7中,在核心库中广泛加入了对 context 的支持,以便更好地控制并发和超时。但在这之前
golang.org/x/net/context 包就一直存在,下一期我们将对 context 包及其应用场景进行讨论。


相关链接:



  1. 原文链接

  2. serial.go

  3. parallel.go

  4. bounded.go

  5. golang.org/x/net/context


扫码关注微信公众号“深入Go语言”


在这里

Go语言并发模型:像Unix Pipe那样使用channel

ming 发表了文章 • 0 个评论 • 498 次浏览 • 2016-10-12 12:25 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


Go语言的并发原语允许开发者以类似于 Unix Pipe 的方式构建数据流水线 (data pipelines),数据流水线能够高效地利用 I/O和多核 CPU 的优势。


本文要讲的就是一些使用流水线的一些例子,流水线的错误处理也是本文的重点。


阅读建议


数据流水线充分利用了多核特性,代码层面是基于 channel 类型 和 go 关键字。


channel 和 go 贯穿本文的始终。如果你对这两个概念不太了解,建议先阅读之前发布的两篇文章:Go 语言内存模型(上/下)


如果你对操作系统中"生产者"和"消费者"模型比较了解的话,也将有助于对本文中流水线的理解。


本文中绝大多数讲解都是基于代码进行的。换句话说,如果你看不太懂某些代码片段,建议补全以后,在机器或play.golang.org 上运行一下。对于某些不明白的细节,可以手动添加一些语句以助于理解。


由于 Go语言并发模型 的英文原文 Go Concurrency Patterns: Pipelines and cancellation 篇幅比较长,本文只包含 理论推导和简单的例子。
下一篇文章我们会对 "并行MD5" 这个现实生活的例子进行详细地讲解。


什么是 "流水线" (pipeline)?


对于"流水线"这个概念,Go语言中并没有正式的定义,它只是很多种并发方式的一种。这里我给出一个非官方的定义:一条流水线是 是由多个阶段组成的,相邻的两个阶段由 channel 进行连接;
每个阶段是由一组在同一个函数中启动的 goroutine 组成。在每个阶段,这些 goroutine 会执行下面三个操作:



  1. 通过 inbound channels 从上游接收数据

  2. 对接收到的数据执行一些操作,通常会生成新的数据

  3. 将新生成的数据通过 outbound channels 发送给下游


除了第一个和最后一个阶段,每个阶段都可以有任意个 inbound 和 outbound channel。
显然,第一个阶段只有 outbound channel,而最后一个阶段只有 inbound channel。
我们通常称第一个阶段为"生产者""源头",称最后一个阶段为"消费者""接收者"


首先,我们通过一个简单的例子来演示这个概念和其中的技巧。后面我们会更出一个真实世界的例子。


流水线入门:求平方数


假设我们有一个流水线,它由三个阶段组成。


第一阶段是 gen 函数,它能够将一组整数转换为channel,channel 可以将数字发送出去。
gen 函数首先启动一个 goroutine,该goroutine 发送数字到 channel,当数字发送完时关闭channel。
代码如下:


func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

第二阶段是 sq 函数,它从 channel 接收一个整数,然后返回 一个channel,返回的channel可以发送 接收到整数的平方。
当它的 inbound channel 关闭,并且把所有数字均发送到下游时,会关闭 outbound channel。代码如下:


func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

main 函数 用于设置流水线并运行最后一个阶段。最后一个阶段会从第二阶段接收数字,并逐个打印出来,直到来自于上游的 inbound channel关闭。代码如下:


func main() {
// 设置流水线
c := gen(2, 3)
out := sq(c)

// 消费输出结果
fmt.Println(<-out) // 4
fmt.Println(<-out) // 9
}

由于 sq 函数的 inbound channel 和 outbound channel 类型一样,所以组合任意个 sq 函数。比如像下面这样使用:


func main() {
// 设置流水线并消费输出结果
for n := range sq(sq(gen(2, 3))) {
fmt.Println(n) // 16 then 81
}
}

如果我们稍微修改一下 gen 函数,便可以模拟 haskell的惰性求值。有兴趣的读者可以自己折腾一下。


流水线进阶:扇入和扇出


扇出:同一个 channel 可以被多个函数读取数据,直到channel关闭。
这种机制允许将工作负载分发到一组worker,以便更好地并行使用 CPU 和 I/O。


扇入:多个 channel 的数据可以被同一个函数读取和处理,然后合并到一个 channel,直到所有 channel都关闭。


下面这张图对 扇入 有一个直观的描述:


扇入


我们修改一下上个例子中的流水线,这里我们运行两个 sq 实例,它们从同一个 channel 读取数据。
这里我们引入一个新函数 merge 对结果进行"扇入"操作:


func main() {
in := gen(2, 3)

// 启动两个 sq 实例,即两个goroutines处理 channel "in" 的数据
c1 := sq(in)
c2 := sq(in)

// merge 函数将 channel c1 和 c2 合并到一起,这段代码会消费 merge 的结果
for n := range merge(c1, c2) {
fmt.Println(n) // 打印 4 9, 或 9 4
}
}

merge 函数 将多个 channel 转换为一个 channel,它为每一个 inbound channel 启动一个 goroutine,用于将数据
拷贝到 outbound channel。
merge 函数的实现见下面代码 (注意 wg 变量):


func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// 为每一个输入channel cs 创建一个 goroutine output
// output 将数据从 c 拷贝到 out,直到 c 关闭,然后 调用 wg.Done
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// 启动一个 goroutine,用于所有 output goroutine结束时,关闭 out
// 该goroutine 必须在 wg.Add 之后启动
go func() {
wg.Wait()
close(out)
}()
return out
}

在上面的代码中,每个 inbound channel 对应一个 output 函数。所有 output goroutine 被创建以后,merge 启动一个额外的 goroutine,
这个goroutine会等待所有 inbound channel 上的发送操作结束以后,关闭 outbound channel。


对已经关闭的channel 执行发送操作(ch<-)会导致异常,所以我们必须保证所有的发送操作都在关闭channel之前结束。
sync.WaitGroup 提供了一种组织同步的方式。
它保证 merge 中所有 inbound channel (cs ...<-chan int) 均被正常关闭, output goroutine 正常结束后,关闭 out channel。


停下来思考一下


在使用流水线函数时,有一个固定的模式:



  1. 在一个阶段,当所有发送操作 (ch<-) 结束以后,关闭 outbound channel

  2. 在一个阶段,goroutine 会持续从 inbount channel 接收数据,直到所有 inbound channel 全部关闭


在这种模式下,每一个接收阶段都可以写成 range 循环的方式,
从而保证所有数据都被成功发送到下游后,goroutine能够立即退出。


在现实中,阶段并不总是接收所有的 inbound 数据。有时候是设计如此:接收者可能只需要数据的一个子集就可以继续执行。
更常见的情况是:由于前一个阶段返回一个错误,导致该阶段提前退出。
这两种情况下,接收者都不应该继续等待后面的值被传送过来。


我们期望的结果是:当后一个阶段不需要数据时,上游阶段能够停止生产。


在我们的例子中,如果一个阶段不能消费所有的 inbound 数据,试图发送这些数据的 goroutine 会永久阻塞。看下面这段代码片段:


    // 只消费 out 的第一个数据
out := merge(c1, c2)
fmt.Println(<-out) // 4 or 9
return
// 由于我们不再接收 out 的第二个数据
// 其中一个 goroutine output 将会在发送时被阻塞
}

显然这里存在资源泄漏。一方面goroutine 消耗内存和运行时资源,另一方面goroutine 栈中的堆引用会阻止 gc 执行回收操作。
既然goroutine 不能被回收,那么他们必须自己退出。


我们重新整理一下流水线中的不同阶段,保证在下游阶段接收数据失败时,上游阶段也能够正常退出。
一个方式是使用带有缓冲的管道作为 outbound channel。缓存可以存储固定个数的数据。
如果缓存没有用完,那么发送操作会立即返回。看下面这段代码示例:


c := make(chan int, 2) // 缓冲大小为 2
c <- 1 // 立即返回
c <- 2 // 立即返回
c <- 3 // 该操作会被阻塞,直到有一个 goroutine 执行 <-c,并接收到数字 1

如果在创建 channel 时就知道要发送的值的个数,使用buffer就能够简化代码。
仍然使用求平方数的例子,我们对 gen 函数进行重写。我们将这组整型数拷贝到一个
缓冲 channel中,从而避免创建一个新的 goroutine:


func gen(nums ...int) <-chan int {
out := make(chan int, len(nums))
for _, n := range nums {
out <- n
}
close(out)
return out
}

回到 流水线中被阻塞的 goroutine,我们考虑让 merge 函数返回一个缓冲管道:


func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int, 1) // 在本例中存储未读的数据足够了
// ... 其他部分代码不变 ...

尽管这种方法解决了这个程序中阻塞 goroutine的问题,但是从长远来看,它并不是好办法。
缓存大小选择为1 是建立在两个前提之上:



  1. 我们已经知道 merge 函数有两个 inbound channel

  2. 我们已经知道下游阶段会消耗多少个值


这段代码很脆弱。如果我们在传入一个值给 gen 函数,或者下游阶段读取的值变少,goroutine
会再次被阻塞。


为了从根本上解决这个问题,我们需要提供一种机制,让下游阶段能够告知上游发送者停止接收的消息。
下面我们看下这种机制。


显式取消 (Explicit cancellation)


当 main 函数决定退出,并停止接收 out 发送的任何数据时,它必须告诉上游阶段的 goroutine 让它们放弃
正在发送的数据。 main 函数通过发送数据到一个名为 done 的channel实现这样的机制。 由于有两个潜在的
发送者被阻塞,它发送两个值。如下代码所示:


func main() {
in := gen(2, 3)

// 启动两个运行 sq 的goroutine
// 两个goroutine的数据均来自于 in
c1 := sq(in)
c2 := sq(in)

// 消耗 output 生产的第一个值
done := make(chan struct{}, 2)
out := merge(done, c1, c2)
fmt.Println(<-out) // 4 or 9

// 告诉其他发送者,我们将要离开
// 不再接收它们的数据
done <- struct{}{}
done <- struct{}{}
}

发送数据的 goroutine 使用一个 select 表达式代替原来的操作,select 表达式只有在接收到 out 或 done
发送的数据后,才会继续进行下去。 done 的值类型为 struct{} ,因为它发送什么值不重要,重要的是它发送没发送:
接收事件发生意味着 channel out 的发送操作被丢弃。 goroutine output 基于 inbound channel c 继续执行
循环,所以上游阶段不会被阻塞。(后面我们会讨论如何让循环提前退出)。 使用 done channel 方式实现的merge 函数如下:


func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// 为 cs 的的每一个 输入channel
// 创建一个goroutine。output函数将
// 数据从 c 拷贝到 out,直到c关闭,
// 或者接收到 done 信号;
// 然后调用 wg.Done()
output := func(c <-chan int) {
for n := range c {
select {
case out <- n:
case <-done:
}
}
wg.Done()
}
// ... the rest is unchanged ...

这种方法有一个问题:每一个下游的接收者需要知道潜在被阻上游发送者的个数,然后向这些发送者发送信号让它们提前退出。
时刻追踪这些数目是一项繁琐且易出错的工作。


我们需要一种方式能够让未知数目、且个数不受限制的goroutine 停止向下游发送数据。在Go语言中,我们可以通过关闭一个
channel 实现,因为在一个已关闭 channel 上执行接收操作(<-ch)总是能够立即返回,返回值是对应类型的零值。关于这点的细节,点击这里查看。


换句话说,我们只要关闭 done channel,就能够让解开对所有发送者的阻塞。对一个管道的关闭操作事实上是对所有接收者的广播信号。


我们把 done channel 作为一个参数传递给每一个 流水线上的函数,通过 defer 表达式声明对 done channel的关闭操作。
因此,所有从 main 函数作为源头被调用的函数均能够收到 done 的信号,每个阶段都能够正常退出。 使用 done 对main函数重构以后,代码如下:


func main() {
// 设置一个 全局共享的 done channel,
// 当流水线退出时,关闭 done channel
// 所有 goroutine接收到 done 的信号后,
// 都会正常退出。
done := make(chan struct{})
defer close(done)

in := gen(done, 2, 3)

// 将 sq 的工作分发给两个goroutine
// 这两个 goroutine 均从 in 读取数据
c1 := sq(done, in)
c2 := sq(done, in)

// 消费 outtput 生产的第一个值
out := merge(done, c1, c2)
fmt.Println(<-out) // 4 or 9

// defer 调用时,done channel 会被关闭。
}

现在,流水线中的每个阶段都能够在 done channel 被关闭时返回。merge 函数中的 output 代码也能够顺利返回,因为它
知道 done channel关闭时,上游发送者 sq 会停止发送数据。 在 defer 表达式执行结束时,所有调用链上的 output 都能保证 wg.Done() 被调用:


func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// 为 cs 的每一个 channel 创建一个 goroutine
// 这个 goroutine 运行 output,它将数据从 c
// 拷贝到 out,直到 c 关闭,或者 接收到 done
// 的关闭信号。人啊后调用 wg.Done()
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
select {
case out <- n:
case <-done:
return
}
}
}
// ... the rest is unchanged ...

同样的原理, done channel 被关闭时,sq 也能够立即返回。在defer表达式执行结束时,所有调用链上的 sq 都能保证
out channel 被关闭。代码如下:


func sq(done <-chan struct{}, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-done:
return
}
}
}()
return out
}

这里,我们给出几条构建流水线的指导:



  1. 当所有发送操作结束时,每个阶段都关闭自己的 outbound channels

  2. 每个阶段都会一直从 inbound channels 接收数据,直到这些 channels 被关闭,或发送者解除阻塞状态。


流水线通过两种方式解除发送者的阻塞:



  1. 提供足够大的缓冲保存发送者发送的数据

  2. 接收者放弃 channel 时,显式地通知发送者。


结论


本文介绍了Go 语言中构建数据流水线的一些技巧。流水线的错误处理比较复杂,流水线的每个阶段都可能阻塞向下游发送数据,
下游阶段也可能不再关注上游发送的数据。上面我们介绍了通过关闭一个channel,向流水线中的所有 goroutine 发送一个 "done" 信号;也定义了
构建流水线的正确方法。


下一篇文章,我们将通过一个 并行 md5 的例子来说明本文所讲的一些理念和技巧。


原作者 Sameer Ajmani,翻译 Oscar


下期预告:Go语言并发模型:以并行md5计算为例。原文链接


相关链接:



  1. 原文链接:https://blog.golang.org/pipelines

  2. Go并发模型:http://talks.golang.org/2012/concurrency.slide#1

  3. Go高级并发模型:http://blog.golang.org/advanced-go-concurrency-patterns


扫码关注微信公众号“深入Go语言”


在这里

Go语言反射三定律

ming 发表了文章 • 0 个评论 • 680 次浏览 • 2016-10-12 12:20 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


Reflection(反射)在计算机中表示 程序能够检查自身结构的能力,尤其是类型。它是元编程的一种形式,也是最容易让人迷惑的一部分。


本文中,我们将解释Go语言中反射的运作机制。每个编程语言的反射模型不大相同,很多语言索性就不支持反射(C、C++)。由于本文是介绍Go语言的,所以当我们谈到“反射”时,默认为是Go语言中的反射。


阅读建议


本文中,我们将解释Go语言中反射的运作机制。每个编程语言的反射模型不大相同,很多语言索性就不支持反射(C、C++)。


由于本文是介绍Go语言的,所以当我们谈到“反射”时,默认为是Go语言中的反射。


虽然Go语言没有继承的概念,但为了便于理解,如果一个struct A 实现了 interface B的所有方法时,我们称之为“继承”。


类型和接口


反射建立在类型系统之上,因此我们从类型基础知识说起。


Go是静态类型语言。每个变量都有且只有一个静态类型,在编译时就已经确定。比如 int、float32、*MyType、[]byte。 如果我们做出如下声明:


type MyInt int

var i int
var j MyInt

上面的代码中,变量 i 的类型是 int,j 的类型是 MyInt。 所以,尽管变量 i 和 j 具有共同的底层类型 int,但它们的静态类型并不一样。不经过类型转换直接相互赋值时,编译器会报错。


关于类型,一个重要的分类是 接口类型(interface),每个接口类型都代表固定的方法集合。一个接口变量就可以存储(或“指向”,接口变量类似于指针)任何类型的具体值,只要这个值实现了该接口类型的所有方法。一组广为人知的例子是 io.Reader 和 io.Writer, Reader 和 Writer 类型来源于 io包,声明如下:


// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}

任何实现了 Read(Write)方法的类型,我们都称之为继承了 io.Reader(io.Writer)接口。换句话说, 一个类型为 io.Reader 的变量 可以指向(接口变量类似于指针)任何类型的变量,只要这个类型实现了Read 方法:


var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

要时刻牢记:不管变量 r 指向的具体值是什么,它的类型永远是 io.Reader。再重复一次:Go语言是静态类型语言,变量 r 的静态类型是 io.Reader。


一个非常非常重要的接口类型是空接口,即:


interface{}

它代表一个空集,没有任何方法。由于任何具体的值都有 零或更多个方法,因此类型为interface{} 的变量能够存储任何值。


有人说,Go的接口是动态类型的。这个说法是错的!接口变量也是静态类型的,它永远只有一个相同的静态类型。如果在运行时它存储的值发生了变化,这个值也必须满足接口类型的方法集合。


由于反射和接口两者的关系很密切,我们必须澄清这一点。


接口变量的表示


Russ Cox 在2009年写了一篇文章介绍 Go中接口变量的表示形式,具体参考文章末尾的链接“Go语言接口的表示”。这里我们不需要重复所有的细节,只做一个简单的总结。


Interface变量存储一对值:赋给该变量的具体的值、值类型的描述符。更准确一点来说,值就是实现该接口的底层数据,类型是底层数据类型的描述。举个例子:


var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty

在这个例子中,变量 r 在结构上包含一个 (value, type) 对:(tty, os.File)。注意:类型 os.File 不仅仅实现了 Read 方法。虽然接口变量只提供 Read 函数的调用权,但是底层的值包含了关于这个值的所有类型信息。所以我们能够做这样的类型转换:


var w io.Writer
w = r.(io.Writer)

上面代码的第二行是一个类型断言,它断定变量 r 内部的实际值也继承了 io.Writer接口,所以才能被赋值给 w。赋值之后,w 就指向了 (tty, *os.File) 对,和变量 r 指向的是同一个 (value, type) 对。不管底层具体值的方法集有多大,由于接口的静态类型限制,接口变量只能调用特定的一些方法。


我们继续往下看:


var empty interface{}
empty = w

这里的空接口变量 empty 也包含 (tty, *os.File) 对。这一点很容易理解:空接口变量可以存储任何具体值以及该值的所有描述信息。


细心的朋友可能会发现,这里没有使用类型断言,因为变量 w 满足 空接口的所有方法(传说中的“无招胜有招”)。在前一个例子中,我们把一个具体值 从 io.Reader 转换为 io.Writer 时,需要显式的类型断言,是因为 io.Writer 的方法集合 不是 io.Reader 的子集。


另外需要注意的一点是,(value, type) 对中的 type 必须是 具体的类型(struct或基本类型),不能是 接口类型。 接口类型不能存储接口变量。


关于接口,我们就介绍到这里,下面我们看看Go语言的反射三定律。


反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”。


注:这里反射类型指 reflect.Typereflect.Value


从用法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部存储的 (value, type) 对。在最开始,我们先了解下 reflect 包的两种类型:Type 和 Value。这两种类型使访问接口内的数据成为可能。它们对应两个简单的方法,分别是 reflect.TypeOf 和 reflect.ValueOf,分别用来读取接口变量的 reflect.Type 和 reflect.Value 部分。当然,从 reflect.Value 也很容易获取到 reflect.Type。目前我们先将它们分开。


首先,我们下看 reflect.TypeOf:


package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}

这段代码会打印出:


type: float64

你可能会疑惑:为什么没看到接口?这段代码看起来只是把一个 float64类型的变量 x 传递给 reflect.TypeOf,并没有传递接口。事实上,接口就在那里。查阅一下TypeOf 的文档,你会发现 reflect.TypeOf 的函数签名里包含一个空接口:


// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

我们调用 reflect.TypeOf(x) 时,x 被存储在一个空接口变量中被传递过去; 然后reflect.TypeOf 对空接口变量进行拆解,恢复其类型信息。


函数 reflect.ValueOf 也会对底层的值进行恢复(这里我们忽略细节,只关注可执行的代码):


var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))

上面这段代码打印出:


value: <float64 Value>

类型 reflect.Type 和 reflect.Value 都有很多方法,我们可以检查和使用它们。这里我们举几个例子。类型 reflect.Value 有一个方法 Type(),它会返回一个 reflect.Type 类型的对象。Type和 Value都有一个名为 Kind 的方法,它会返回一个常量,表示底层数据的类型,常见值有:Uint、Float64、Slice等。Value类型也有一些类似于Int、Float的方法,用来提取底层的数据。Int方法用来提取 int64, Float方法用来提取 float64,参考下面的代码:


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())

上面这段代码会打印出:


type: float64
kind is float64: true
value: 3.4

还有一些用来修改数据的方法,比如SetInt、SetFloat,在讨论它们之前,我们要先理解“可修改性”(settability),这一特性会在“反射第三定律”中进行详细说明。


反射库提供了很多值得列出来单独讨论的属性。首先是介绍下Value 的 getter 和 setter 方法。为了保证API 的精简,这两个方法操作的是某一组类型范围最大的那个。比如,处理任何含符号整型数,都使用 int64。也就是说 Value 类型的Int 方法返回值为 int64类型,SetInt 方法接收的参数类型也是 int64 类型。实际使用时,可能需要转化为实际的类型:


var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint()) // v.Uint returns a uint64.

第二个属性是反射类型变量(reflection object)的 Kind 方法 会返回底层数据的类型,而不是静态类型。如果一个反射类型对象包含一个用户定义的整型数,看代码:


type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

上面的代码中,虽然变量 v 的静态类型是MyInt,不是 int,Kind 方法仍然返回 reflect.Int。换句话说, Kind 方法不会像 Type 方法一样区分 MyInt 和 int。


反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”。


和物理学中的反射类似,Go语言中的反射也能创造自己反面类型的对象。


根据一个 reflect.Value 类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回。其函数声明如下:


// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

然后,我们可以通过断言,恢复底层的具体值:


y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

上面这段代码会打印出一个 float64 类型的值,也就是 反射类型变量 v 所代表的值。


事实上,我们可以更好地利用这一特性。标准库中的 fmt.Println 和 fmt.Printf 等函数都接收空接口变量作为参数,fmt 包内部会对接口变量进行拆包(前面的例子中,我们也做过类似的操作)。因此,fmt 包的打印函数在打印 reflect.Value 类型变量的数据时,只需要把 Interface 方法的结果传给 格式化打印程序:


fmt.Println(v.Interface())

你可能会问:问什么不直接打印 v ,比如 fmt.Println(v)? 答案是 v 的类型是 reflect.Value,我们需要的是它存储的具体值。由于底层的值是一个 float64,我们可以格式化打印:


fmt.Printf("value is %7.1e\n", v.Interface())

上面代码的打印结果是:


3.4e+00

同样,这次也不需要对 v.Interface() 的结果进行类型断言。空接口值内部包含了具体值的类型信息,Printf 函数会恢复类型信息。


简单来说,Interface 方法和 ValueOf 函数作用恰好相反,唯一一点是,返回值的静态类型是 interface{}。


我们重新表述一下:Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。


反射第三定律:如果要修改“反射类型对象”,其值必须是“可写的”(settable)。


这条定律很微妙,也很容易让人迷惑。但是如果你从第一条定律开始看,应该比较容易理解。


下面这段代码不能正常工作,但是非常值得研究:


var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你运行这段代码,它会抛出抛出一个奇怪的异常:


panic: reflect.Value.SetFloat using unaddressable value

这里问题不在于值 7.1 不能被寻址,而是因为变量 v 是“不可写的”。“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。


我们可以通过 CanSet 方法检查一个 reflect.Value 类型变量的“可写性”。对于上面的例子,可以这样写:


var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

上面这段代码打印结果是:


settability of v: false

对于一个不具有“可写性”的 Value类型变量,调用 Set 方法会报出错误。首先,我们要弄清楚什么“可写性”。


“可写性”有些类似于寻址能力,但是更严格。它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。“可写性”最终是由一个事实决定的:反射对象是否存储了原始值。举个代码例子:


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

这里我们传递给 reflect.ValueOf 函数的是变量 x 的一个拷贝,而非 x 本身。想象一下,如果下面这行代码能够成功执行:


v.SetFloat(7.1)

答案是:如果这行代码能够成功执行,它不会更新 x ,虽然看起来变量 v 是根据 x 创建的。相反,它会更新 x 存在于 反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。


这看起来很诡异,事实上并非如此,而且类似的情况很常见。考虑下面这行代码:


f(x)

上面的代码中,我们把变量 x 的一个拷贝传递给函数,因此不期望它会改变 x 的值。如果期望函数 f 能够修改变量 x,我们必须传递 x 的地址(即指向 x 的指针)给函数 f,如下:


f(&x)

你应该很熟悉这行代码,反射的工作机制是一样的。如果你想通过反射修改变量 x,就咬吧想要修改的变量的指针传递给 反射库。


首先,像通常一样初始化变量 x,然后创建一个指向它的 反射对象,名字为 p:


var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

这段代码的输出是:


type of p: *float64
settability of p: false

反射对象 p 是不可写的,但是我们也不像修改 p,事实上我们要修改的是 *p。为了得到 p 指向的数据,可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”,然后将结果存储到反射 Value类型对象 v中:


v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

在上面这段代码中,变量 v 是一个可写的反射对象,代码输出也验证了这一点:


settability of v: true

由于变量 v 代表 x, 因此我们可以使用 v.SetFloat 修改 x 的值:


v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

上面代码的输出如下:


7.1
7.1

反射不太容易理解,reflect.Type 和 reflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言做的事情。你只需要记住:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。


结构体(struct)


在前面的例子中,变量 v 本身并不是指针,它只是从指针衍生而来。把反射应用到结构体时,常用的方式是 使用反射修改一个结构体的某些字段。只要拥有结构体的地址,我们就可以修改它的字段。


下面通过一个简单的例子对结构体类型变量 t 进行分析。


首先,我们创建了反射类型对象,它包含一个结构体的指针,因为后续会修改。


然后,我们设置 typeOfT 为它的类型,并遍历所有的字段。


注意:我们从 struct 类型提取出每个字段的名字,但是每个字段本身也是常规的 reflect.Value 对象。


type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}

上面这段代码的输出如下:


0: A int = 23
1: B string = skidoo

这里还有一点需要指出:变量 T 的字段都是首字母大写的(暴露到外部),因为struct中只有暴露到外部的字段才是“可写的”。


由于变量 s 包含一个“可写的”反射对象,我们可以修改结构体的字段:


f.Interface())s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

上面代码的输出如下:


t is now {77 Sunset Strip}

如果变量 s 是通过 t ,而不是 &t 创建的,调用 SetInt 和 SetString 将会失败,因为 t 的字段不是“可写的”。


结论


最后再次重复一遍反射三定律:



  1. 反射可以将“接口类型变量”转换为“反射类型对象”。

  2. 反射可以将“反射类型对象”转换为“接口类型变量”。

  3. 如果要修改“反射类型对象”,其值必须是“可写的”(settable)。
    一旦你理解了这些定律,使用反射将会是一件非常简单的事情。它是一件强大的工具,使用时务必谨慎使用,更不要滥用。


关于反射,我们还有很多内容没有讨论,包括基于管道的发送和接收、内存分配、使用slice和map、调用方法和函数,由于本文已经非常长了,这些话题在后续的文章中介绍。


原作者 Rob Pike,翻译Oscar


相关链接:


原文链接:https://blog.golang.org/laws-of-reflection


reflect 包:https://golang.org/pkg/reflect/


扫码关注微信公众号“深入Go语言”


在这里

深入理解Go语言的slice

ming 发表了文章 • 1 个评论 • 504 次浏览 • 2016-10-12 12:09 • 来自相关话题

先看这段代码,结果是[0 2 3],很多人都能答对。

func modify(s []int) {
    s[0] = 0
}
func ... 			查看全部
					

先看这段代码,结果是[0 2 3],很多人都能答对。


func modify(s []int) {
s[0] = 0
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s)
}

然后稍微改动一下,再猜一下结果


func pop(s []int) {
s = s[:len(s)-1]
}
func main() {
s := []int{1, 2, 3}
pop(s)
fmt.Println(s)
}

如果认为输出[1 2]的话那么你错了,结果是[1 2 3],你可能会觉得很奇怪,slice是引用语义这个在第一个例子中已经证明了,为什么第二个例子中又不是这样呢。


我们对中间过程加一些输出,再来看看


func pop(s []int) {
fmt.Printf("[pop] s addr:%p\n", &s)
s = s[:len(s)-1]
fmt.Println("[pop] s value:", s)
}
func main() {
s := []int{1, 2, 3}
fmt.Printf("[main] s addr:%p\n", &s)
pop(s)
fmt.Println("[main] s value:", s)
}

运行上面代码输出如下


[main] s addr:0xc082004640
[pop] s addr:0xc0820046c0
[pop] s value: [1 2]
[main] s value: [1 2 3]

看到上面的结果,可以知道pop()中的s并不是引用,而是一个副本,虽然在pop()内部修改成功,但并没有影响到main()中的s。但第一个例子却修改成功了,这又是为什么。


下面来看下slice的实现,就能很清楚的了解原因了。

slice是由长度固定的数组实现的。当使用内建函数append()向slice添加元素时,如果超过底层的数组长度则会重新分配空间(与C++的vector类似)。

可以把slice认为是下面这样的一个结构体(先不考虑slice的容量)。Lenght表示slice的长度,`ZerothElement表示底层数组的头指针


type sliceHeader struct {
Length int
ZerothElement *byte
}

参照这个结构体的定义和下面的说明,就能很清楚地了解开始的两个例子了


clipboard.png


那当我们需要将slice做为函数参数传入,并且函数会修改slice时,怎么办呢。这里说三种方法。
1.将slice指针做为参数,而不是slice


func modify(s *[]int) {
// do something
}

2.把函数内被修改后的slice做为返回值,将函数返回值赋值给原始slice


func modify(s []int) []int {
// do something
return s
}
func main() {
s := []int{1, 2, 3}
s = modify(s)
}

3.将函数做为slice指针的方法


type slice []int

func (s *slice) modify() {
// do something
}

深入理解go的slice和到底什么时候该用slice

sheepbao 发表了文章 • 1 个评论 • 404 次浏览 • 2016-10-12 12:09 • 来自相关话题

前言

用过go语言的亲们都知道,slice(中文翻译为切片)在编程中经常用到,它代表变长的序列,序列中每个元素都有相同的类型,类似一个动态数组,利用append可以实现动态增长,利用slice的特性可以很容易的切割slice,它们是怎么... 查看全部

前言


用过go语言的亲们都知道,slice(中文翻译为切片)在编程中经常用到,它代表变长的序列,序列中每个元素都有相同的类型,类似一个动态数组,利用append可以实现动态增长,利用slice的特性可以很容易的切割slice,它们是怎么实现这些特性的呢?现在我们来探究一下这些特性的本质是什么。


先了解一下slice的特性



  • 定义一个slice
    s := []int{1,2,3,4,5}
    fmt.Println(s) // [1 2 3 4 5]

    一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。


  • slice的扩容
    s := []int{1,2,3,4,5}
    s = append(s, 6)
    fmt.Println(s) // [1 2 3 4 5 6]

    内置append函数在现有数组的长度 < 1024 时 cap 增长是翻倍的,再往上的增长率则是 1.25,至于为何后面会说。


  • slice的切割
    s := []int{1,2,3,4,5,6}
    s1 := s[0:2]
    fmt.Println(s1) // [1 2]
    s2 := s[4:]
    fmt.Println(s2) // [5 6]
    s3 := s[:4]
    fmt.Println(s3) // [1 2 3 4]


  • slice作为函数参数


    package main

    import "fmt"

    func main() {

    slice_1 := []int{1, 2, 3, 4, 5}
    fmt.Printf("main-->data:\t%#v\n", slice_1)
    fmt.Printf("main-->len:\t%#v\n", len(slice_1))
    fmt.Printf("main-->cap:\t%#v\n", cap(slice_1))
    test1(slice_1)
    fmt.Printf("main-->data:\t%#v\n", slice_1)

    test2(&slice_1)
    fmt.Printf("main-->data:\t%#v\n", slice_1)

    }

    func test1(slice_2 []int) {
    slice_2[1] = 6666 // 函数外的slice确实有被修改
    slice_2 = append(slice_2, 8888) // 函数外的不变
    fmt.Printf("test1-->data:\t%#v\n", slice_2)
    fmt.Printf("test1-->len:\t%#v\n", len(slice_2))
    fmt.Printf("test1-->cap:\t%#v\n", cap(slice_2))
    }

    func test2(slice_2 *[]int) { // 这样才能修改函数外的slice
    *slice_2 = append(*slice_2, 6666)
    }

    结果:


    main-->data:    []int{1, 2, 3, 4, 5}
    main-->len: 5
    main-->cap: 5
    test1-->data: []int{1, 6666, 3, 4, 5, 8888}
    test1-->len: 6
    test1-->cap: 12
    main-->data: []int{1, 6666, 3, 4, 5}
    main-->data: []int{1, 6666, 3, 4, 5, 6666}

    这里要注意注释的地方,为何slice作为值传递参数,函数外的slice也被更改了?为何在函数内append不能改变函数外的slice?要回da这些问题就得了解slice内部结构,详细请看下面.




slice的内部结构


其实slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定义:


struct    Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};

这个结构有3个字段,第一个字段表示array的指针,就是真实数据的指针(这个一定要注意),所以才经常说slice是数组的引用,第二个是表示slice的长度,第三个是表示slice的容量,注意:len和cap都不是指针


现在就可以解释前面的例子slice作为函数参数提出的问题:
函数外的slice叫slice_1,函数的参数叫slice_2,当函数传递slice_1的时候,其实传入的确实是slice_1参数的复制,所以slice_2复制了slise_1,但要注意的是slice_2里存储的数组的指针,所以当在函数内更改数组内容时,函数外的slice_1的内容也改变了。在函数内用append时,append会自动以倍增的方式扩展slice_2的容量,但是扩展也仅仅是函数内slice_2的长度和容量,slice_1的长度和容量是没变的,所以在函数外打印时看起来就是没变。


append的运作机制


在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:



  • 如果新的slice大小是当前大小2倍以上,则大小增长为新大小

  • 否则循环以下操作:如果当前slice大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

  • append的实现只是简单的在内存中将旧slice复制给新slice


至于为何会这样,你要看一下golang的源码就知道了:
https://github.com/golang/go/blob/master/src/runtime/slice.go


    newcap := old.cap
if newcap+newcap < cap {
newcap = cap
} else {
for {
if old.len < 1024 {
newcap += newcap
} else {
newcap += newcap / 4
}
if newcap >= cap {
break
}
}
}

为何不用动态链表实现slice?



  • 首先拷贝一断连续的内存是很快的,假如不想发生拷贝,也就是用动态链表,那你就没有连续内存。此时随机访问开销会是:链表 O(N), 2倍增长块链 O(LogN),二级表一个常数很大的O(1)。问题不仅是算法上开销,还有内存位置分散而对缓存高度不友好,这些问题i在连续内存方案里都是不存在的。除非你的应用是狂append然后只顺序读一次,否则优化写而牺牲读都完全不 make sense. 而就算你的应用是严格顺序读,缓存命中率也通常会让你的综合效率比拷贝换连续内存低。

  • 对小 slice 来说,连续 append 的开销更多的不是在 memmove, 而是在分配一块新空间的 memory allocator 和之后的 gc 压力(这方面对链表更是不利)。所以,当你能大致知道所需的最大空间(在大部分时候都是的)时,在make的时候预留相应的 cap 就好。如果所需的最大空间很大而每次使用的空间量分布不确定,那你就要在浪费内存和耗 CPU 在 allocator + gc 上做权衡。

  • Go 在 append 和 copy 方面的开销是可预知+可控的,应用上简单的调优有很好的效果。这个世界上没有免费的动态增长内存,各种实现方案都有设计权衡。


什么时候该用slice?


在go语言中slice是很灵活的,大部分情况都能表现的很好,但也有特殊情况。
当程序要求slice的容量超大并且需要频繁的更改slice的内容时,就不应该用slice,改用list更合适。