Js 计算文件MD5的两种方式
1 | // 方式1:使用crypto-js |
实测使用spark-md5计算要快一些
1 | // 方式1:使用crypto-js |
实测使用spark-md5计算要快一些
网上找到的两种方法,先记录下备用。
1 |
|
1 |
|
1 | /** |
1 | /** |
下面提供一份更全的编码字节表,使用这个表可以实现Base256等
1 | private static final char[] symbolTable = { |
1 | private static final Charset CHARSET = Charset.forName("GB2312"); |
ADD:我在网上找到了一篇更完整的文章,文章中详细介绍了,这么配合使用alfred + iTerm2 + ssh config快速连接远程服务器:https://juejin.cn/post/6844903909916426248
Alfred执行命令时默认使用的是MaxOS系统自带的Terminal,而我平时使用的都是iTerm。在Github上找到了解决方案:https://github.com/vitorgalvao/custom-alfred-iterm-scripts
设置方式直接截图说明:
配置内容如下(这个内容是我从上面那个Github项目中Copy出来的,可以自行去项目中Copy最新的):
1 | -- For the latest version: |
这也备份一下修改之前的配置,防止丢失:
1 | on alfred_script(q) |
1 | public class IpMatcher { |
当时是为了测试文件网卡问题,发现同样的文件,在一个很烂的网卡中不断的下载,计算的文件MD5会不一样的问题。代码记录下,留作参考。
1 | package main |
1 | package main |
新建文件:pm2.json
1 | { |
然后执行命令:pm2 start pm2.json
启动服务。
电脑上是使用nvm安装的node,安装之后每次打开一个新的terminal都比较慢,运营是因为termainl在打开的时候会加载nvm相关内容,如下:
1 | # nvm |
可以按照下面方式来修改,只有在需要用到nvm的时候才去加载:
1 | # nvm |
推荐一个工具:https://github.com/jenv/jenv
Follow the steps below to get a working jenv
installation with knowledge of your java
environment. Read all the code you execute carefully: a $
symbol at the beginning of a line should be omitted, since it’s meant to show you entering a command into your terminal and observing the response after the command.
jenv
On OSX, the simpler way to install jEnv is using Homebrew
1 | brew install jenv |
Alternatively, and on Linux, you can install it from source :
1 | git clone https://github.com/jenv/jenv.git ~/.jenv |
Restart your shell by closing and reopening your terminal window or running exec $SHELL -l
in the current session for the changes to take effect.
To verify jenv
was installed, run jenv doctor
. On a macOS machine, you’ll observe the following output:
1 | jenv doctor |
Observe that jenv
is correctly loaded but Java is not yet installed.
To make sure JAVA_HOME
is set, make sure to enable the export
plugin:
1 | jenv enable-plugin export |
Problem? Please visit the Trouble Shooting Wiki page.
Continue to the next section to install java.
Untested: While this fork has improved fish
shell support, it has not been tested by this maintainer. To install jenv
for Fish according to the contributor’s instructions:
1 | echo 'set PATH $HOME/.jenv/bin $PATH' >> ~/.config/fish/config.fish |
Use jenv add
to inform jenv
where your Java environment is located. jenv
does not, by itself, install Java.
For example, on macOS, use brew
to install the latest Java (OpenJDK 11) followed by the appropriate jenv add PATH_TO_JVM_HOME
command to recognize it.
1 | brew install --cask java |
With macOS OpenJDK 11.0.2 installed, for example, either of these commands will add /Library/Java/JavaVirtualMachines/openjdk-11.0.2.jdk/Contents/Home
as a valid JVM. Your JVM directory may vary!
Observe now that this version of Java is added to your java versions
command:
1 | jenv versions |
By default, the latest version of Java is your system
Java on macOS.
We’ll now set a jenv local VERSION
local Java version for the current working directory. This will create a .java-version
file we can check into Git for our projects, and jenv
will load it correctly when a shell is started from this directory.
1 | jenv local 11.0.2 |
Is JAVA_HOME
set?
1 | echo ${JAVA_HOME} |
Yes! Observe that JAVA_HOME
is set to a valid shim directory. Unlike the main repository’s documentation we helpfully installed the export
plugin, and we now have the most important jenv
features covered.
If you executed this commands inside your $HOME
directory, you can now delete .java-version
:
1 | rm .java-version |
Use jenv global VERSION
to set a global Java version:
1 | jenv global 11.0.2 |
When you next open a shell or terminal window, this version of Java will be the default.
On macOS, this sets JAVA_HOME
for GUI applications on macOS using jenv macos-javahome
. Integrates this tutorial to create a file that does not update dynamically depending on what local or shell version of Java is set, only global.
Use jenv shell VERSION
to set the Java used in this particular shell session:
1 | jenv shell 11.0.2 |
These common workflows demonstrate how to use jenv
to solve common problems.
Our goal is to have both the latest version of Java and JDK 8 installed at the same time. This is helpful for developing Android applications, whose build tools are sensitive to using an exact Java version.
We’ll resume where we left off with Java 11.0.2 installed. Let’s install Java 8 now:
1 | brew install --cask adoptopenjdk8 |
This will install the latest version of Java 8 to a special directory in macOS. Let’s see which directory that is:
1 | ls -1 /Library/Java/JavaVirtualMachines |
Observe the adoptopenjdk-8.jdk
directory. Your exact version may vary. We cannot retrieve this using /usr/libexec/java_home
, unfortunately. We’ll add the Java home directory using jenv
so that it shows up in our jenv versions
command:
1 | jenv add /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home/ |
场景:目录中有多个git项目,想要更新批量拉取一下这些项目的最新代码。
实现如下:
1 |
|
扫码目录下的所有项目,如果发现当前项目在master分支,那么pull一下代码。
偶然的机会需要用一下PHP,记录下安装方式。本身mac系统是自带php的,但是自带的修改起来及其不方便,不好安装扩展。所以直接使用brew安装。
1 | brew search php 使用此命令搜索可用的PHP版本 |
1 | pecl version 查看版本信息 |
本文主要介绍mac
环境下使用iterm2
的rz sz
功能的安装流程。
1 | brew install lrzsz |
将iterm2-send-zmodem.sh
和iterm2-recv-zmodem.sh
保存到/usr/local/bin
目录下。
1 | #!/bin/bash |
1 | #!/bin/bash |
1 | chmod 777 /usr/local/bin/iterm2-* |
设置Iterm2的Tirgger特性,profiles->default->editProfiles->Advanced中的Tirgger
添加两条trigger,分别设置 Regular expression,Action,Parameters,Instant如下:
1 | Regular expression: rz waiting to receive.\*\*B0100 |
示例图:
参考:
今天有需求将一些非常大的图片压缩一下,本来想自己写代码进行压缩的,但是觉得这是一个非常常见的需求,应该有现成的解决方案,于是Google了一下,找到了这两个工具:jpegoptim、optipng
我是在MacOS中安装的,Linux上应该也有这个两个工具,请自行摸索
我使用的是brew
进行安装,命令如下:
1 | brew install jpegoptim |
1 | 压缩 |
1 | optipng file.png |
启动mumu模拟器之后在设备列表中找不到模拟器,于是在网上搜索了下教程。
有个教程提供一下方法:
1 | adb connect 127.0.0.1:7555 |
附模拟器端口:
1 | 夜神模拟器:adb connect 127.0.0.1:62001 |
但是这个方法在mac下好像不还用,于是又找到另外一种方法:
1 | adb kill-server && adb server && adb shell |
这个方法终于生效。
1 | yum install mariadb-server |
注意:安装之后初始密码为空
1 | systemctl start mariadb #启动服务 |
执行命令mysql_secure_installation
进行初始化,过程中会让你设置root
密码等信息,自己按照提示一步步来即可。
1 | [root@iZj6chtv8h63huh6sbynuiZ ~]# mysql_secure_installation |
系统支持:CentOS,Debian,Ubuntu
内存要求:≥128M
日期:2015年08月01日
一键安装 go 版的 shadowsocks 最新版本 1.1.4。据说 go 版本有 buff 。与 python 版不同的是,其客户端程序能使用多个服务端配置,本脚本安装的是服务端程序。作者默认推荐 aes-128-cfb 加密,基于一致性,脚本使用了 aes-256-cfb 加密方式。
服务器端口:自己设定(如不设定,默认为 8989)
客户端端口:1080
密码:自己设定(如不设定,默认为teddysun.com)
http://sourceforge.net/projects/shadowsocksgui/files/dist/
使用root用户登录,运行以下命令:
1 | wget --no-check-certificate https://raw.githubusercontent.com/iMeiji/shadowsocks_install/master/shadowsocks-go.sh |
1 | Congratulations, shadowsocks-go install completed! |
使用 root 用户登录,运行以下命令:
1 | ./shadowsocks-go.sh uninstall |
客户端配置的参考链接:http://teddysun.com/339.html
安装完成后即已后台启动 shadowsocks-go ,运行:
1 | /etc/init.d/shadowsocks status |
可以查看 shadowsocks-go 进程是否已经启动。
本脚本安装完成后,已将 shadowsocks-go 加入开机自启动。
启动:/etc/init.d/shadowsocks start
停止:/etc/init.d/shadowsocks stop
重启:/etc/init.d/shadowsocks restart
状态:/etc/init.d/shadowsocks status
多用户多端口配置文件 sample(2015年01月08日):
配置文件路径:vi /etc/shadowsocks/config.json
1 | { |
参考链接:
https://github.com/shadowsocks/shadowsocks-go
https://github.com/iMeiji/shadowsocks_install/wiki/shadowsocks-go-%E4%B8%80%E9%94%AE%E5%AE%89%E8%A3%85
MySql导出整库或者指定表的数据使用mysqldump
命令即可,但是导出表中指定列的数据就需要用到下面命令了,如下:
1 | mysql -uroot -p123456 database_name -e "SELECT name from t_xxx where type = 3 INTO OUTFILE'/data/xxx.sql'" |
用sudo时提示”xxx is not in the sudoers file. This incident will be reported.其中XXX是你的用户名,也就是你的用户名没有权限使用sudo,我们只要修改一下/etc/sudoers文件就行了。下面是修改方法:
chmod u+w /etc/sudoers
。vim /etc/sudoers
,输入”i”进入编辑模式,找到这一 行:root ALL=(ALL) ALL
在起下面添加xxx ALL=(ALL) ALL
(这里的xxx是你的用户名),然后保存(就是先按一 下Esc键,然后输入”:wq”)退出。chmod u-w /etc/sudoers
。 原文地址:https://levelup.gitconnected.com/use-go-channels-as-promises-and-async-await-ee62d93078ec
If you’ve ever programmed with Javascript, you definitely know about Promise
and async
/await
. C#, *Java, Python, *and some other programming languages apply the same pattern but with other names such as Task
or Future
.
On the contrary, Go doesn’t follow the pattern at all. Instead, it introduces goroutines
and channels
. However, it isn’t difficult to replicate the pattern with goroutines
and channels
.
First, let’s experiment with a simple use case: await
a result from an async
function.
1 | // Javascript. |
1 | // Go. |
Single async/await in Javascript vs. Golang
To declare an “async” function in Go:
<-chan ReturnType
.make(chan ReturnType)
and return the created channel at the end of the function.go func() {...}
and implement the function’s logic inside that anonymous function.defer close(r)
to close the channel once done.To “await” the result, simply read the value from channel by v := <- fn()
.
It’s very common that we start multiple async tasks then wait for all of them to finish and gather their results. Doing that is quite simple in both Javascript and Golang.
1 | // Javascript. |
1 | // Go. |
We have to do it in 2 lines of code and introduce 3 more variables, but it’s clean and simple enough.
We can not do <-longRun(), <-longRun(), <-longRun()
, which will longRun()
one by one instead all in once.
Sometimes, a piece of data can be received from several sources to avoid high latencies, or there’re cases that multiple results are generated but they’re equivalent and the only first response is consumed. This first-response-win pattern, therefore, is quite popular. Achieving that in both Javascript and Go is very simple.
1 | // Javascript. |
1 | // Go. |
select-case
is the pattern that Go designed specifically for racing channel operations. We can even do more stuff within each case, but we’re focusing only on the result so we just leave them all empty.
Because Go’s error propagation model is very different from Javascript, there’s any clean way to replicate Promise.then()
and Promise.catch()
. In Go, error is returned along with return values instead of being thrown as exception. Therefore, if your function can fail, you can consider changing your return <-chan ReturnType
into <-chan ReturnAndErrorType
, which is a struct holding both the result and error.
SpringBoot 中可以基于 ControllerAdvice
和 HttpMessageConverter
实现对数据返回的包装。
实现如下,先来写一个 POJO
来定义一下返回格式:
1 | import com.example.demo.common.exception.base.ErrorCode; |
这里用到了
lombok
,lombok
的使用介绍不在本文范围内。
用一个 ResponseBodyAdvice
类的实现包装 Controller
的返回值:
以下是我以前的实现方式:
1 | import com.example.demo.common.RequestContextHolder; |
为什么要对返回类型是 String
时进行特殊处理呢?因为如果直接返回 new Response<>(body)
的话,在使用时返回 String
类型的话,会报类型转换异常,当时也没有理解什么原因导致的,所以最后使用了 jackson
对 Response
又做了一次序列化。
今天找到了导致这个异常的原因:
因为在所有的
HttpMessageConverter
实例集合中,StringHttpMessageConverter
要比其它的Converter
排得靠前一些。我们需要将处理Object
类型的HttpMessageConverter
放得靠前一些,这可以在Configuration
类中完成:
1 | import org.springframework.context.annotation.Configuration; |
然后 FormatResponseBodyAdvice
就可以修改为如下实现:
1 | import org.springframework.core.MethodParameter; |
比之前的实现方式优雅了很多而且不用再处理 jackson
的异常了。
写一个 Controller
来尝试一下:
1 |
|
请求这个端点得到结果:
1 | { |
说明我们的配置是成功的,同时可以在相应头中看到:
1 | content-type: application/json;charset=UTF-8 |
如果是之前的实现方式,这里的值就是:
1 | content-type: html/text |
也不太符合 restful
规范。
转载自:https://jpanj.com/2018/SpringBoot-%E4%B8%AD%E7%BB%9F%E4%B8%80%E5%8C%85%E8%A3%85%E5%93%8D%E5%BA%94/
1 | 1. Checkout |
好博客导航
数据没有分页的问题。好博客导航
页面布局,页面更美观。大家好,我在接下来的两个会议中的目标是向大家提供有关编写 Go 代码最佳实践的建议。
这是一个研讨会形式的演讲,不会有幻灯片,而是直接从文档开始。
贴士: 在这里有最新的文章链接
https://dave.cheney.net/practical-go/presentations/qcon-china.html
如果我要谈论任何编程语言的最佳实践,我需要一些方法来定义“什么是最佳”。如果你昨天来到我的主题演讲,你会看到 Go 团队负责人 Russ Cox 的这句话:
Software engineering is what happens to programming when you add time and other programmers. (软件工程就是你和其他程序员花费时间在编程上所发生的事情。)
— Russ Cox
Russ 作出了软件编程与软件工程的区分。 前者是你自己写的一个程序。 后者是很多人会随着时间的推移而开发的产品。 工程师们来来去去,团队会随着时间增长与缩小,需求会发生变化,功能会被添加,错误也会得到修复。 这是软件工程的本质。
我可能是这个房间里 Go 最早的用户之一,但要争辩说我的资历给我的看法更多是假的。相反,今天我要提的建议是基于我认为的 Go 语言本身的指导原则:
注意:
你会注意到我没有说性能或并发。 有些语言比 Go 语言快一点,但它们肯定不像 Go 语言那么简单。 有些语言使并发成为他们的最高目标,但它们并不具有可读性及生产力。
性能和并发是重要的属性,但不如简单性,可读性和生产力那么重要。
我们为什么要追求简单? 为什么 Go 语言程序的简单性很重要?
我们都曾遇到过这样的情况: “我不懂这段代码”,不是吗? 我们都做过这样的项目:你害怕做出改变,因为你担心它会破坏程序的另一部分; 你不理解的部分,不知道如何修复。
这就是复杂性。 复杂性把可靠的软件中变成不可靠。 复杂性是杀死软件项目的罪魁祸首。
简单性是 Go 语言的最高目标。 无论我们编写什么程序,我们都应该同意这一点:它们很简单。
Readability is essential for maintainability.
(可读性对于可维护性是至关重要的。)
— Mark Reinhold (2018 JVM 语言高层会议)
为什么 Go 语言的代码可读性是很重要的?我们为什么要争取可读性?
Programs must be written for people to read, and only incidentally for machines to execute. (程序应该被写来让人们阅读,只是顺便为了机器执行。)
— Hal Abelson 与 Gerald Sussman (计算机程序的结构与解释)
可读性很重要,因为所有软件不仅仅是 Go 语言程序,都是由人类编写的,供他人阅读。执行软件的计算机则是次要的。
代码的读取次数比写入次数多。一段代码在其生命周期内会被读取数百次,甚至数千次。
The most important skill for a programmer is the ability to effectively communicate ideas. (程序员最重要的技能是有效沟通想法的能力。)
— Gastón Jorquera [1]
可读性是能够理解程序正在做什么的关键。如果你无法理解程序正在做什么,那你希望如何维护它?如果软件无法维护,那么它将被重写;最后这可能是你的公司最后一次投资 Go 语言。
如果你正在为自己编写一个程序,也许它只需要运行一次,或者你是唯一一个曾经看过它的人,然后做任何对你有用的事。但是,如果是一个不止一个人会贡献编写的软件,或者在很长一段时间内需求、功能或者环境会改变,那么你的目标必须是你的程序可被维护。
编写可维护代码的第一步是确保代码可读。
Design is the art of arranging code to work today, and be changeable forever. (设计是安排代码到工作的艺术,并且永远可变。)
— Sandi Metz
我要强调的最后一个基本原则是生产力。开发人员的工作效率是一个庞大的主题,但归结为此; 你花多少时间做有用的工作,而不是等待你的工具或迷失在一个外国的代码库里。 Go 程序员应该觉得他们可以通过 Go 语言完成很多工作。
有人开玩笑说, Go 语言是在等待 C++ 语言程序编译时设计的。快速编译是 Go 语言的一个关键特性,也是吸引新开发人员的关键工具。虽然编译速度仍然是一个持久的战场,但可以说,在其他语言中需要几分钟的编译,在 Go 语言中只需几秒钟。这有助于 Go 语言开发人员感受到与使用动态语言的同行一样的高效,而且没有那些语言固有的可靠性问题。
对于开发人员生产力问题更为基础的是,Go 程序员意识到编写代码是为了阅读,因此将读代码的行为置于编写代码的行为之上。Go 语言甚至通过工具和自定义强制执行所有代码以特定样式格式化。这就消除了项目中学习特定格式的摩擦,并帮助发现错误,因为它们看起来不正确。
Go 程序员不会花费整天的时间来调试不可思议的编译错误。他们也不会将浪费时间在复杂的构建脚本或在生产中部署代码。最重要的是,他们不用花费时间来试图了解他们的同事所写的内容。
当他们说语言必须扩展时,Go 团队会谈论生产力。
我们要讨论的第一个主题是标识符。 标识符是一个用来表示名称的花哨单词; 变量的名称,函数的名称,方法的名称,类型的名称,包的名称等。
Poor naming is symptomatic of poor design. (命名不佳是设计不佳的症状。)
— Dave Cheney
鉴于 Go 语言的语法有限,我们为程序选择的名称对我们程序的可读性产生了非常大的影响。 可读性是良好代码的定义质量,因此选择好名称对于 Go 代码的可读性至关重要。
Obvious code is important. What you can do in one line you should do in three.
(清晰的代码很重要。在一行可以做的你应当分三行做。(if/else
吗?))
— Ukiah Smith
Go 语言不是为了单行而优化的语言。 Go 语言不是为了最少行程序而优化的语言。我们没有优化源代码的大小,也没有优化输入所需的时间。
Good naming is like a good joke. If you have to explain it, it’s not funny.
(好的命名就像一个好笑话。如果你必须解释它,那就不好笑了。)
— Dave Cheney
清晰的关键是在 Go 语言程序中我们选择的标识名称。让我们谈一谈所谓好的名字:
好的名字很简洁。 好的名字不一定是最短的名字,但好的名字不会浪费在无关的东西上。好名字具有高的信噪比。
好的名字是描述性的。 好的名字会描述变量或常量的应用,而不是它们的内容。好的名字应该描述函数的结果或方法的行为,而不是它们的操作。好的名字应该描述包的目的而非它的内容。描述东西越准确的名字就越好。
好的名字应该是可预测的。 你能够从名字中推断出使用方式。这是选择描述性名称的功能,但它也遵循传统。这是 Go 程序员在谈到习惯用语时所谈论的内容。
让我们深入讨论以下这些属性。
有时候人们批评 Go 语言推荐短变量名的风格。正如 Rob Pike 所说,“ Go 程序员想要正确的长度的标识符”。 [1]
Andrew Gerrand 建议通过对某些事物使用更长的标识,向读者表明它们具有更高的重要性。
The greater the distance between a name’s declaration and its uses, the longer the name should be. (名字的声明与其使用之间的距离越大,名字应该越长。)
— Andrew Gerrand [2]
由此我们可以得出一些指导方针:
我们来举个栗子:
1 | type Person struct { |
在此示例中,变量 p
的在第 10
行被声明并且也只在接下来的一行中被引用。 p
在执行函数期间存在时间很短。如果要了解 p
的作用只需阅读两行代码。
相比之下,people
在函数第 7
行参数中被声明。sum
和 count
也是如此,他们用了更长的名字。读者必须查看更多的行数来定位它们,因此他们名字更为独特。
我可以选择 s
替代 sum
以及 c
(或可能是 n
)替代 count
,但是这样做会将程序中的所有变量份量降低到同样的级别。我可以选择 p
来代替 people
,但是用什么来调用 for ... range
迭代变量。如果用 person
的话看起来很奇怪,因为循环迭代变量的生命时间很短,其名字的长度超出了它的值。
贴士:
与使用段落分解文档的方式一样用空行来分解函数。 在AverageAge
中,按顺序共有三个操作。 第一个是前提条件,检查people
是否为空,第二个是sum
和count
的累积,最后是平均值的计算。
重要的是要意识到关于命名的大多数建议都是需要考虑上下文的。 我想说这是一个原则,而不是一个规则。
两个标识符 i
和 index
之间有什么区别。 我们不能断定一个就比另一个好,例如
1 | for index := 0; index < len(s); index++ { |
从根本上说,上面的代码更具有可读性
1 | for i := 0; i < len(s); i++ { |
我认为它不是,因为就此事而论, i
和 index
的范围很大可能上仅限于 for 循环的主体,后者的额外冗长性(指 index
)几乎没有增加对于程序的理解。
但是,哪些功能更具可读性?
1 | func (s *SNMP) Fetch(oid []int, index int) (int, error) |
或
1 | func (s *SNMP) Fetch(o []int, i int) (int, error) |
在此示例中,oid
是 SNMP
对象 ID
的缩写,因此将其缩短为 o
意味着程序员必须要将文档中常用符号转换为代码中较短的符号。 类似地将 index
替换成 i
,模糊了 i
所代表的含义,因为在 SNMP
消息中,每个 OID
的子值称为索引。
贴士: 在同一声明中长和短形式的参数不能混搭。
你不应该用变量的类型来命名你的变量, 就像您不会将宠物命名为“狗”和“猫”。 出于同样的原因,您也不应在变量名字中包含类型的名字。
变量的名称应描述其内容,而不是内容的类型。 例如:
1 | var usersMap map[string]*User |
这个声明有什么好处? 我们可以看到它是一个 map
,它与 *User
类型有关。 但是 usersMap
是一个 map
,而 Go 语言是一种静态类型的语言,如果没有定义变量,不会让我们意外地使用到它,因此 Map
后缀是多余的。
接下来, 如果我们像这样来声明其他变量:
1 | var ( |
usersMap
,companiesMap
和 productsMap
三个 map
类型变量,所有映射字符串都是不同的类型。 我们知道它们是 map
,我们也知道我们不能使用其中一个来代替另一个 - 如果我们在需要 map[string]*User
的地方尝试使用 companiesMap
, 编译器将抛出错误异常。 在这种情况下,很明显变量中 Map
后缀并没有提高代码的清晰度,它只是增加了要输入的额外样板代码。
我的建议是避免使用任何类似变量类型的后缀。
贴士:
如果users
的描述性都不够用,那么usersMap
也不会。
此建议也适用于函数参数。 例如:
1 | type Config struct { |
命名 *Config
参数 config
是多余的。 我们知道它是 *Config
类型,就是这样。
在这种情况下,如果变量的生命周期足够短,请考虑使用 conf
或 c
。
如果有更多的 *Config
,那么将它们称为 original
和 updated
比 conf1
和 conf2
会更具描述性,因为前者不太可能被互相误解。
贴士:
不要让包名窃取好的变量名。
导入标识符的名称包括其包名称。 例如,context
包中的Context
类型将被称为context.Context
。 这使得无法将context
用作包中的变量或类型。
1 | func WriteLog(context context.Context, message string) |
上面的栗子将会编译出错。 这就是为什么
context.Context
类型的通常的本地声明是ctx
,例如:
1 | func WriteLog(ctx context.Context, message string) |
一个好名字的另一个属性是它应该是可预测的。 在第一次遇到该名字时读者就能够理解名字的使用。 当他们遇到常见的名字时,他们应该能够认为自从他们上次看到它以来它没有改变意义。
例如,如果您的代码在处理数据库请确保每次出现参数时,它都具有相同的名称。 与其使用 d * sql.DB
,dbase * sql.DB
,DB * sql.DB
和 database * sql.DB
的组合,倒不如统一使用:
1 | db *sql.DB |
这样做使读者更为熟悉; 如果你看到db
,你知道它就是 *sql.DB
并且它已经在本地声明或者由调用者为你提供。
类似地,对于方法接收器: 在该类型的每个方法上使用相同的接收者名称。 在这种类型的方法内部可以使读者更容易使用。
注意:
Go 语言中的短接收者名称惯例与目前提供的建议不一致。 这只是早期做出的选择之一,已经成为首选的风格,就像使用CamelCase
而不是snake_case
一样。
贴士:
Go 语言样式规定接收器具有单个字母名称或从其类型派生的首字母缩略词。 你可能会发现接收器的名称有时会与方法中参数的名称冲突。 在这种情况下,请考虑将参数名称命名稍长,并且不要忘记一致地使用此新参数名称。
最后,某些单字母变量传统上与循环和计数相关联。 例如,i
,j
和 k
通常是简单 for
循环的循环归纳变量。n
通常与计数器或累加器相关联。v
是通用编码函数中值的常用简写,k
通常用于 map
的键,s
通常用作字符串类型参数的简写。
与上面的 db
示例一样,程序员认为 i
是一个循环归纳变量。 如果确保 i
始终是循环变量,而且不在 for
循环之外的其他地方中使用。 当读者遇到一个名为 i
或 j
的变量时,他们知道循环就在附近。
贴士:
如果你发现自己有如此多的嵌套循环,i
,j
和k
变量都无法满足时,这个时候可能就是需要将函数分解成更小的函数。
Go 至少有六种不同的方式来声明变量
var x int = 1
var x = 1
var x int; x = 1
var x = int(1)
x := 1
我确信还有更多我没有想到的。 这可能是 Go 语言的设计师意识到的一个错误,但现在改变它为时已晚。 通过所有这些不同的方式来声明变量,我们如何避免每个 Go 程序员选择自己的风格?
我想就如何在程序中声明变量提出建议。 这是我尽可能使用的风格。
声明变量但没有初始化时,请使用 var
。 当声明变量稍后将在函数中初始化时,请使用 var
关键字。
1 | var players int // 0 |
var
表示此变量已被声明为指定类型的零值。 这也与使用 var
而不是短声明语法在包级别声明变量的要求一致 - 尽管我稍后会说你根本不应该使用包级变量。
在声明和初始化时,使用 :=
。 在同时声明和初始化变量时,也就是说我们不会将变量初始化为零值,我建议使用短变量声明。 这使得读者清楚地知道 :=
左侧的变量是初始化过的。
为了解释原因,让我们看看前面的例子,但这次是初始化每个变量:
1 | var players int = 0 |
在第一个和第三个例子中,因为在 Go 语言中没有从一种类型到另一种类型的自动转换; 赋值运算符左侧的类型必须与右侧的类型相同。 编译器可以从右侧的类型推断出声明的变量的类型,上面的例子可以更简洁地写为:
1 | var players = 0 |
我们将 players
初始化为 0
,但这是多余的,因为 0
是 players
的零值。 因此,要明确地表示使用零值, 我们将上面例子改写为:
1 | var players int |
第二个声明如何? 我们不能省略类型而写作:
1 | var things = nil |
因为 nil
没有类型。 [2]相反,我们有一个选择,如果我们要使用切片的零值则写作:
1 | var things []Thing |
或者我们要创建一个有零元素的切片则写作:
1 | var things = make([]Thing, 0) |
如果我们想要后者那么这不是切片的零值,所以我们应该向读者说明我们通过使用简短的声明形式做出这个选择:
1 | things := make([]Thing, 0) |
这告诉读者我们已选择明确初始化事物。
下面是第三个声明,
1 | var thing = new(Thing) |
既是初始化了变量又引入了一些 Go 程序员不喜欢的 new
关键字的罕见用法。 如果我们用推荐地简短声明语法,那么就变成了:
1 | thing := new(Thing) |
这清楚地表明 thing
被初始化为 new(Thing)
的结果 - 一个指向 Thing
的指针 - 但依旧我们使用了 new
地罕见用法。 我们可以通过使用紧凑的文字结构初始化形式来解决这个问题,
1 | thing := &Thing{} |
与 new(Thing)
相同,这就是为什么一些 Go 程序员对重复感到不满。 然而,这意味着我们使用指向 Thing{}
的指针初始化了 thing
,也就是 Thing
的零值。
相反,我们应该认识到 thing
被声明为零值,并使用地址运算符将 thing
的地址传递给 json.Unmarshall
1 | var thing Thing |
贴士:
当然,任何经验法则,都有例外。 例如,有时两个变量密切相关,这样写会很奇怪:
1 | var min int |
如果这样声明可能更具可读性
1 | min, max := 0, 1000 |
综上所述:
在没有初始化的情况下声明变量时,请使用 var
语法。
声明并初始化变量时,请使用 :=
。
贴士:
使复杂的声明显而易见。
当事情变得复杂时,它看起来就会很复杂。例如
1 | var length uint32 = 0x80 |
这里
length
可能要与特定数字类型的库一起使用,并且length
明确选择为uint32
类型而不是短声明形式:
1 | length := uint32(0x80) |
在第一个例子中,我故意违反了规则, 使用
var
声明带有初始化变量的。 这个决定与我的常用的形式不同,这给读者一个线索,告诉他们一些不寻常的事情将会发生。
我谈到了软件工程的目标,即编写可读及可维护的代码。 因此,您可能会将大部分职业生涯用于你不是唯一作者的项目。 我在这种情况下的建议是遵循项目自身风格。
在文件中间更改样式是不和谐的。 即使不是你喜欢的方式,对于维护而言一致性比你的个人偏好更有价值。 我的经验法则是: 如果它通过了 gofmt
,那么通常不值得再做代码审查。
贴士:
如果要在代码库中进行重命名,请不要将其混合到另一个更改中。 如果有人使用git bisect
,他们不想通过数千行重命名来查找您更改的代码。
在我们继续讨论更大的项目之前,我想花几分钟时间谈论一下注释。
Good code has lots of comments, bad code requires lots of comments.
(好的代码有很多注释,坏代码需要很多注释。)
— Dave Thomas and Andrew Hunt (The Pragmatic Programmer)
注释对 Go 语言程序的可读性非常重要。 注释应该做的三件事中的一件:
第一种形式是公共符号注释的理想选择:
1 | // Open opens the named file for reading. |
第二种形式非常适合在方法中注释:
1 | // queue all dependant actions |
第三种形式是独一无二的,因为它不会取代前两种形式,但与此同时它并不能代替前两种形式。 此形式的注解用以解释代码的外部因素。 这些因素脱离上下文后通常很难理解,此注释的为了提供这种上下文。
1 | return &v2.Cluster_CommonLbConfig{ |
在此示例中,无法清楚地明白 HealthyPanicThreshold
设置为零百分比的效果。 需要注释 0
值将禁用 panic
阀值。
我之前谈过,变量或常量的名称应描述其目的。 向变量或常量添加注释时,该注释应描述变量内容,而不是变量目的。
1 | const randomNumber = 6 // determined from an unbiased die |
在此示例中,注释描述了为什么 randomNumber
被赋值为6,以及6来自哪里。 注释没有描述 randomNumber
的使用位置。 还有更多的栗子:
1 | const ( |
在HTTP的上下文中,数字 100
被称为 StatusContinue
,如 RFC 7231 第 6.2.1 节中所定义。
贴士:
对于没有初始值的变量,注释应描述谁负责初始化此变量。
1 | // sizeCalculationDisabled indicates whether it is safe |
这里的注释让读者知道
dowidth
函数负责维护sizeCalculationDisabled
的状态。隐藏在众目睽睽下
这个提示来自Kate Gregory[3]。有时你会发现一个更好的变量名称隐藏在注释中。
1 | // registry of SQL drivers |
注释是由作者添加的,因为
registry
没有充分解释其目的 - 它是一个注册表,但注册的是什么?通过将变量重命名为
sqlDrivers
,现在可以清楚地知道此变量的目的是保存SQL驱动程序。
1 | var sqlDrivers = make(map[string]*sql.Driver) |
之前的注释就是多余的,可以删除。
godoc
是包的文档,所以应该始终为包中声明的每个公共符号 — 变量、常量、函数以及方法添加注释。
以下是 Google Style
指南中的两条规则:
1 | package ioutil |
1 | // Read implements the io.Reader interface |
这是 io
包中的一个例子
1 | // LimitReader returns a Reader that reads from r |
请注意,LimitedReader
的声明就在使用它的函数之前,而 LimitedReader.Read
的声明遵循 LimitedReader
本身的声明。 尽管 LimitedReader.Read
本身没有文档,但它清楚地表明它是 io.Reader
的一个实现。
贴士:
在编写函数之前,请编写描述函数的注释。 如果你发现很难写出注释,那么这就表明你将要编写的代码很难理解。
Don’t comment bad code — rewrite it
— Brian Kernighan
粗劣的代码的注释高亮显示是不够的。 如果你遇到其中一条注释,则应提出问题,以提醒您稍后重构。 只要技术债务数额已知,它是可以忍受的。
标准库中的惯例是注意到它的人用 TODO(username)
的样式来注释。
1 | // TODO(dfc) this is O(N^2), find a faster way to do this. |
注释 username
不是该人承诺要解决该问题,但在解决问题时他们可能是最好的人选。 其他项目使用 TODO
与日期或问题编号来注释。
Good code is its own best documentation. As you’re about to add a comment, ask yourself, ‘How can I improve the code so that this comment isn’t needed?’ Improve the code and then document it to make it even clearer.
好的代码是最好的文档。 在即将添加注释时,请问下自己,“如何改进代码以便不需要此注释?’ 改进代码使其更清晰。
— Steve McConnell
函数应该只做一件事。 如果你发现自己在注释一段与函数的其余部分无关的代码,请考虑将其提取到它自己的函数中。
除了更容易理解之外,较小的函数更易于隔离测试,将代码隔离到函数中,其名称可能是所需的所有文档。
Write shy code - modules that don’t reveal anything unnecessary to other modules and that don’t rely on other modules’ implementations.
编写谨慎的代码 - 不向其他模块透露任何不必要的模块,并且不依赖于其他模块的实现。
— Dave Thomas
每个 Go 语言的包实际上都是它一个小小的 Go 语言程序。 正如函数或方法的实现对调用者而言并不重要一样,包的公共API-其函数、方法以及类型的实现对于调用者来说也并不重要。
一个好的 Go 语言包应该具有低程度的源码级耦合,这样,随着项目的增长,对一个包的更改不会跨代码库级联。 这些世界末日的重构严格限制了代码库的变化率以及在该代码库中工作的成员的生产率。
在本节中,我们将讨论如何设计包,包括包的名称,命名类型以及编写方法和函数的技巧。
编写一个好的 Go 语言包从包的名称开始。将你的包名用一个词来描述它。
正如我在上一节中谈到变量的名称一样,包的名称也非常重要。我遵循的经验法则不是“我应该在这个包中放入什么类型的?”。相反,我要问是“该包提供的服务是什么?”通常这个问题的答案不是“这个包提供 X
类型”,而是“这个包提供 HTTP
”。
贴士:
以包所提供的内容来命名,而不是它包含的内容。
在项目中,每个包名称应该是唯一的。包的名称应该描述其目的的建议很容易理解 - 如果你发现有两个包需要用相同名称,它可能是:
base
,common
或 util
的包名称不好的包名的常见情况是 utility
包。这些包通常是随着时间的推移一些帮助程序和工具类的包。由于这些包包含各种不相关的功能,因此很难根据包提供的内容来描述它们。这通常会导致包的名称来自包含的内容 - utilities
。
像 utils
或 helper
这样的包名称通常出现在较大的项目中,这些项目已经开发了深层次包的结构,并且希望在不遇到导入循环的情况下共享 helper
函数。通过将 utility
程序函数提取到新的包中,导入循环会被破坏,但由于该包源于项目中的设计问题,因此其包名称不反映其目的,仅反映其为了打破导入循环。
我建议改进 utils
或 helpers
包的名称是分析它们的调用位置,如果可能的话,将相关的函数移动到调用者的包中。即使这涉及复制一些 helper
程序代码,这也比在两个程序包之间引入导入依赖项更好。
[A little] duplication is far cheaper than the wrong abstraction.
([一点点]重复比错误的抽象的性价比高很多。)
— Sandy Metz
在使用 utility
程序的情况下,最好选多个包,每个包专注于单个方面,而不是选单一的整体包。
贴士:
使用复数形式命名utility
包。例如strings
来处理字符串。
当两个或多个实现共有的功能或客户端和服务器的常见类型被重构为单独的包时,通常会找到名称类似于 base
或 common
的包。我相信解决方案是减少包的数量,将客户端,服务器和公共代码组合到一个以包的功能命名的包中。
例如,net/http
包没有 client
和 server
的分包,而是有一个 client.go
和 server.go
文件,每个文件都有各自的类型,还有一个 transport.go
文件,用于公共消息传输代码。
贴士:
标识符的名称包括其包名称。
重要的是标识符的名称包括其包的名称。
- 当由另一个包引用时,
net/http
包中的 Get 函数变为http.Get
。- 当导入到其他包中时,
strings
包中的Reader
类型变为strings.Reader
。net
包中的Error
接口显然与网络错误有关。
return
而不是深度嵌套由于 Go 语言的控制流不使用 exception
,因此不需要为 try
和 catch
块提供顶级结构而深度缩进代码。Go 语言代码不是成功的路径越来越深地嵌套到右边,而是以一种风格编写,其中随着函数的进行,成功路径继续沿着屏幕向下移动。 我的朋友 Mat Ryer 将这种做法称为“视线”编码。[4]
这是通过使用 guard clauses
来实现的; 在进入函数时是具有断言前提条件的条件块。 这是一个来自 bytes
包的例子:
1 | func (b *Buffer) UnreadRune() error { |
进入 UnreadRune
后,将检查 b.lastRead
的状态,如果之前的操作不是 ReadRune
,则会立即返回错误。 之后,函数的其余部分继续进行 b.lastRead
大于 opInvalid
的断言。
与没有 guard clause
的相同函数进行比较,
1 | func (b *Buffer) UnreadRune() error { |
最常见的执行成功的情况是嵌套在第一个if条件内,成功的退出条件是 return nil
,而且必须通过仔细匹配大括号来发现。 函数的最后一行是返回一个错误,并且被调用者必须追溯到匹配的左括号,以了解何时执行到此点。
对于读者和维护程序员来说,这更容易出错,因此 Go 语言更喜欢使用 guard clauses
并尽早返回错误。
假设变量没有初始化,每个变量声明都会自动初始化为与零内存的内容相匹配的值。 这就是零值。 值的类型决定了其零值; 对于数字类型,它为 0
,对于指针类型为 nil
,slices
、map
和 channel
同样是 nil
。
始终设置变量为已知默认值的属性对于程序的安全性和正确性非常重要,并且可以使 Go 语言程序更简单、更紧凑。 这就是 Go 程序员所说的“给你的结构一个有用的零值”。
对于 sync.Mutex
类型。sync.Mutex
包含两个未公开的整数字段,它们用来表示互斥锁的内部状态。 每当声明 sync.Mutex
时,其字段会被设置为 0
初始值。sync.Mutex
利用此属性来编写,使该类型可直接使用而无需初始化。
1 | type MyInt struct { |
另一个利用零值的类型是 bytes.Buffer
。您可以声明 bytes.Buffer
然后就直接写入而无需初始化。
1 | func main() { |
切片的一个有用属性是它们的零值 nil
。如果我们看一下切片运行时 header
的定义就不难理解:
1 | type slice struct { |
此结构的零值意味着 len
和 cap
的值为 0
,而 array
(指向保存切片的内容数组的指针)将为 nil
。这意味着你不需要 make
切片,你只需声明它即可。
1 | func main() { |
注意:
var s []string
类似于它上面的两条注释行,但并不完全相同。值为nil
的切片与具有零长度的切片就可以来相互比较。以下代码将输出false
。
1 | func main() { |
nil pointers
– 未初始化的指针变量的一个有用属性是你可以在具有 nil
值的类型上调用方法。它可以简单地用于提供默认值。
1 | type Config struct { |
编写可维护程序的关键是它们应该是松散耦合的 - 对一个程序包的更改应该很少影响另一个不直接依赖于第一个程序包的程序包。
在 Go 语言中有两种很好的方法可以实现松散耦合
在 Go 语言中,我们可以在函数或方法范围以及包范围内声明变量。当变量是公共的时,给定一个以大写字母开头的标识符,那么它的范围对于整个程序来说实际上是全局的 - 任何包都可以随时观察该变量的类型和内容。
可变全局状态引入程序的独立部分之间的紧密耦合,因为全局变量成为程序中每个函数的不可见参数!如果该变量的类型发生更改,则可以破坏依赖于全局变量的任何函数。如果程序的另一部分更改了该变量,则可以破坏依赖于全局变量状态的任何函数。
如果要减少全局变量所带来的耦合,
我们来谈谈如何将包组合到项目中。 通常一个项目是一个 git
仓库,但在未来 Go 语言开发人员会交替地使用 module
和 project
。
就像一个包,每个项目都应该有一个明确的目的。 如果你的项目是一个库,它应该提供一件事,比如 XML
解析或记录。 您应该避免在一个包实现多个目的,这将有助于避免成为 common
库。
贴士:
据我的经验,common
库最终会与其最大的调用者紧密相连,在没有升级该库与最大调用者的情况下是很难修复的,还会带来了许多无关的更改以及API破坏。
如果你的项目是应用程序,如 Web
应用程序,Kubernetes
控制器等,那么项目中可能有一个或多个 main
程序包。 例如,我编写的 Kubernetes
控制器有一个 cmd/contour
包,既可以作为部署到 Kubernetes
集群的服务器,也可以作为调试目的的客户端。
对于从其他语言过渡到 Go 语言的程序员来说,我倾向于在代码审查中提到的一件事是他们会过度使用包。
Go 语言没有提供有关可见性的详细方法; Java有 public
、protected
、private
以及隐式 default
的访问修饰符。 没有 C++
的 friend
类概念。
在 Go 语言中,我们只有两个访问修饰符,public
和 private
,由标识符的第一个字母的大小写表示。 如果标识符是公共的,则其名称以大写字母开头,该标识符可用于任何其他 Go 语言包的引用。
注意:
你可能会听到人们说exported
与not exported
, 跟public
和private
是同义词。
鉴于包的符号的访问有限控件,Go 程序员应遵循哪些实践来避免创建过于复杂的包层次结构?
贴士:
除cmd/
和internal/
之外的每个包都应包含一些源代码。
我的建议是选择更少,更大的包。 你应该做的是不创建新的程序包。 这将导致太多类型被公开,为你的包创建一个宽而浅的API。
以下部分将更为详细地探讨这一建议。
贴士:
来自Java
?
如果您来自Java
或C#
,请考虑这一经验法则 –Java
包相当于单个.go
源文件。 - Go 语言包相当于整个Maven
模块或.NET
程序集。
import
语句将代码排列到文件中如果你按照包提供的内容来安排你的程序包,是否需要对 Go 包中的文件也执行相同的操作?什么时候应该将 .go
文件拆分成多个文件?什么时候应该考虑整合 .go
文件?
以下是我的经验法则:
.go
文件。为该文件指定与文件夹名称相同的名称。例如: package http
应放在名为 http
的目录中名为 http.go
的文件中。messages.go
包含 Request
和 Response
类型,client.go
包含 Client
类型,server.go
包含 Server
类型。import
的声明类似,请考虑将它们组合起来。或者确定 import
集之间的差异并移动它们。messages.go
可能负责网络的 HTTP
请求和响应,http.go
可能包含底层网络处理逻辑,client.go
和 server.go
实现 HTTP
业务逻辑请求的实现或路由等等。贴士: 首选名词为源文件命名。
注意:
Go编译器并行编译每个包。 在一个包中,编译器并行编译每个函数(方法只是 Go 语言中函数的另一种写法)。 更改包中代码的布局不会影响编译时间。
go tool
支持在两个地方编写 testing
包测试。假设你的包名为 http2
,您可以编写 http2_test.go
文件并使用包 http2
声明。这样做会编译 http2_test.go
中的代码,就像它是 http2
包的一部分一样。这就是内部测试。
go tool
还支持一个特殊的包声明,以 test
为结尾,即 package http_test
。这允许你的测试文件与代码一起存放在同一个包中,但是当编译时这些测试不是包的代码的一部分,它们存在于自己的包中。就像调用另一个包的代码一样来编写测试。这被称为外部测试。
我建议在编写单元测试时使用内部测试。这样你就可以直接测试每个函数或方法,避免外部测试干扰。
但是,你应该将 Example
测试函数放在外部测试文件中。这确保了在 godoc
中查看时,示例具有适当的包名前缀并且可以轻松地进行复制粘贴。
贴士:
避免复杂的包层次结构,抵制应用分类法
Go 语言包的层次结构对于go tool
没有任何意义除了下一节要说的。 例如,net/http
包不是一个子包或者net
包的子包。如果在项目中创建了不包含
.go
文件的中间目录,则可能无法遵循此建议。
internal
包来减少公共API如果项目包含多个包,可能有一些公共的函数,这些函数旨在供项目中的其他包使用,但不打算成为项目的公共API的一部分。 如果你发现是这种情况,那么 go tool
会识别一个特殊的文件夹名称 - 而非包名称 - internal/
可用于放置对项目公开的代码,但对其他项目是私有的。
要创建此类包,请将其放在名为 internal/
的目录中,或者放在名为 internal/
的目录的子目录中。 当 go
命令在其路径中看到导入包含 internal
的包时,它会验证执行导入的包是否位于 internal
目录。
例如,.../a/b/c/internal/d/e/f
的包只能通过以 .../a/b/c/
为根目录的代码被导入。 它无法通过 .../a/b/g
或任何其他仓库中的代码导入。[5]
main
包内容尽可能的少main
函数和 main
包的内容应尽可能少。 这是因为 main.main
充当单例; 程序中只能有一个 main
函数,包括 tests
。
因为 main.main
是一个单例,假设 main
函数中需要执行很多事情,main.main
只会在 main.main
或 main.init
中调用它们并且只调用一次。 这使得为 main.main
编写代码测试变得很困难,因此你应该将所有业务逻辑从 main
函数中移出,最好是从 main
包中移出。
贴士:
main
应该做解析flags
,开启数据库连接、开启日志等,然后将执行交给更高一级的对象。
我今天要给出的最后一条建议是设计, 我认为也是最重要的。
到目前为止我提出的所有建议都是建议。 这些是我尝试编写 Go 语言的方式,但我不打算在代码审查中拼命推广。
但是,在审查 API 时, 我就不会那么宽容了。 这是因为到目前为止我所谈论的所有内容都是可以修复而且不会破坏向后兼容性; 它们在很大程度上是实现的细节。
当涉及到软件包的公共 API 时,在初始设计中投入大量精力是值得的,因为稍后更改该设计对于已经使用 API 的人来说会是破坏性的。
APIs should be easy to use and hard to misuse.
(API 应该易于使用且难以被误用)
— Josh Bloch [3]
如果你从这个演讲中带走任何东西,那应该是 Josh Bloch 的建议。 如果一个 API 很难用于简单的事情,那么 API 的每次调用都会很复杂。 当 API 的实际调用很复杂时,它就会便得不那么明显,而且会更容易被忽视。
简单, 但难以正确使用的 API 是采用两个或更多相同类型参数的 API。 让我们比较两个函数签名:
1 | func Max(a, b int) int |
这两个函数有什么区别? 显然,一个返回两个数字最大的那个,另一个是复制文件,但这不重要。
1 | Max(8, 10) // 10 |
Max
是可交换的; 参数的顺序无关紧要。 无论是 8 比 10 还是 10 比 8,最大的都是 10。
但是,却不适用于 CopyFile
。
1 | CopyFile("/tmp/backup", "presentation.md") |
这些声明中哪一个备份了 presentation.md
,哪一个用上周的版本覆盖了 presentation.md
? 没有文档,你无法分辨。 如果没有查阅文档,代码审查员也无法知道你写对了顺序。
一种可能的解决方案是引入一个 helper
类型,它会负责如何正确地调用 CopyFile
。
1 | type Source string |
通过这种方式,CopyFile
总是能被正确调用 - 还可以通过单元测试 - 并且可以被设置为私有,进一步降低了误用的可能性。
贴士: 具有多个相同类型参数的API难以正确使用。
几年前,我就对 functional options
[7] 进行过讨论[6],使 API 更易用于默认用例。
本演讲的主旨是你应该为常见用例设计 API。 另一方面, API 不应要求调用者提供他们不在乎参数。
nil
作为参数本章开始时我建议是不要强迫提供给 API 的调用者他们不在乎的参数。 这就是我要说的为默认用例设计 API。
这是 net/http
包中的一个例子
1 | package http |
ListenAndServe
有两个参数,一个用于监听传入连接的 TCP
地址,另一个用于处理 HTTP
请求的 http.Handler
。Serve
允许第二个参数为 nil
,需要注意的是调用者通常会传递 nil
,表示他们想要使用 http.DefaultServeMux
作为隐含参数。
现在,Serve
的调用者有两种方式可以做同样的事情。
1 | http.ListenAndServe("0.0.0.0:8080", nil) |
两者完全相同。
这种 nil
行为是病毒式的。 http
包也有一个 http.Serve
帮助类,你可以合理地想象一下 ListenAndServe
是这样构建的
1 | func ListenAndServe(addr string, handler Handler) error { |
因为 ListenAndServe
允许调用者为第二个参数传递 nil
,所以 http.Serve
也支持这种行为。 事实上,http.Serve
实现了如果 handler
是nil
,使用 DefaultServeMux
的逻辑。 参数可为 nil
可能会导致调用者认为他们可以为两个参数都使用 nil
。 像下面这样:
1 | http.Serve(nil, nil) |
会导致 panic
。
贴士:
不要在同一个函数签名中混合使用可为nil
和不能为nil
的参数。
http.ListenAndServe
的作者试图在常见情况下让使用 API 的用户更轻松些,但很可能会让该程序包更难以被安全地使用。
使用 DefaultServeMux
或使用 nil
没有什么区别。
1 | const root = http.Dir("/htdocs") |
对比
1 | const root = http.Dir("/htdocs") |
这种混乱值得拯救吗?
1 | const root = http.Dir("/htdocs") |
贴士: 认真考虑
helper
函数会节省不少时间。 清晰要比简洁好。
贴士:
避免公共 API 使用测试参数
避免在公开的 API 上使用仅在测试范围上不同的值。 相反,使用Public wrappers
隐藏这些参数,使用辅助方式来设置测试范围中的属性。
[]T
参数编写一个带有切片参数的函数或方法是很常见的。
1 | func ShutdownVMs(ids []string) error |
这只是我编的一个例子,但它与我所写的很多代码相同。 这里的问题是他们假设他们会被调用于多个条目。 但是很多时候这些类型的函数只用一个参数调用,为了满足函数参数的要求,它必须打包到一个切片内。
另外,因为 ids
参数是切片,所以你可以将一个空切片或 nil
传递给该函数,编译也没什么错误。 但是这会增加额外的测试负载,因为你应该涵盖这些情况在测试中。
举一个这类 API 的例子,最近我重构了一条逻辑,要求我设置一些额外的字段,如果一组参数中至少有一个非零。 逻辑看起来像这样:
1 | if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 { |
由于 if
语句变得很长,我想将签出的逻辑拉入其自己的函数中。 这就是我提出的:
1 | // anyPostive indicates if any value is greater than zero. |
这就能够向读者明确内部块的执行条件:
1 | if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) { |
但是 anyPositive
还存在一个问题,有人可能会这样调用它:
1 | if anyPositive() { ... } |
在这种情况下,anyPositive
将返回 false
,因为它不会执行迭代而是立即返回 false
。对比起如果 anyPositive
在没有传递参数时返回 true
, 这还不算世界上最糟糕的事情。
然而,如果我们可以更改 anyPositive
的签名以强制调用者应该传递至少一个参数,那会更好。我们可以通过组合正常和可变参数来做到这一点,如下所示:
1 | // anyPostive indicates if any value is greater than zero. |
现在不能使用少于一个参数来调用 anyPositive
。
假设我需要编写一个将 Document
结构保存到磁盘的函数的任务。
1 | // Save writes the contents of doc to the file f. |
我可以指定这个函数 Save
,它将 *os.File
作为写入 Document
的目标。但这样做会有一些问题
Save
的签名排除了将数据写入网络位置的选项。假设网络存储可能在以后成为需求,则此功能的签名必须改变,从而影响其所有调用者。
Save
测试起来也很麻烦,因为它直接操作磁盘上的文件。因此,为了验证其操作,测试时必须在写入文件后再读取该文件的内容。
而且我必须确保 f
被写入临时位置并且随后要将其删除。
*os.File
还定义了许多与 Save
无关的方法,比如读取目录并检查路径是否是符号链接。 如果 Save
函数的签名只用 *os.File
的相关内容,那将会很有用。
我们能做什么 ?
1 | // Save writes the contents of doc to the supplied |
使用 io.ReadWriteCloser
,我们可以应用接口隔离原则来重新定义 Save
以获取更通用文件形式。
通过此更改,任何实现 io.ReadWriteCloser
接口的类型都可以替换以前的 *os.File
。
这使 Save
在其应用程序中更广泛,并向 Save
的调用者阐明 *os.File
类型的哪些方法与其操作有关。
而且,Save
的作者也不可以在 *os.File
上调用那些不相关的方法,因为它隐藏在 io.ReadWriteCloser
接口后面。
但我们可以进一步采用接口隔离原则。
首先,如果 Save
遵循单一功能原则,它不可能读取它刚刚写入的文件来验证其内容 - 这应该是另一段代码的功能。
1 | // Save writes the contents of doc to the supplied |
因此,我们可以将我们传递给 Save
的接口的规范缩小到只写和关闭。
其次,通过向 Save
提供一个关闭其流的机制,使其看起来仍然像一个文件,这就提出了在什么情况下关闭 wc
的问题。
可能 Save
会无条件地调用 Close
,或者在成功的情况下调用 Close
。
这给 Save
的调用者带来了问题,因为它可能希望在写入文档后将其他数据写入流。
1 | // Save writes the contents of doc to the supplied |
一个更好的解决方案是重新定义 Save
仅使用 io.Writer
,它只负责将数据写入流。
将接口隔离原则应用于我们的 Save
功能,同时, 就需求而言, 得出了最具体的一个函数 - 它只需要一个可写的东西 - 并且它的功能最通用,现在我们可以使用 Save
将我们的数据保存到实现 io.Writer
的任何事物中。
[译注: 不理解设计原则部分的同学可以阅读 Dave 大神的另一篇《Go 语言 SOLID 设计》]
我已经给出了几个关于错误处理的演示文稿[8],并在我的博客上写了很多关于错误处理的文章。我在昨天的会议上也讲了很多关于错误处理的内容,所以在这里不再赘述。
相反,我想介绍与错误处理相关的两个其他方面。
如果你昨天在我的演讲中,我谈到了改进错误处理的提案。但是你知道有什么比改进错误处理的语法更好吗?那就是根本不需要处理错误。
注意:
我不是说“删除你的错误处理”。我的建议是,修改你的代码,这样就不用处理错误了。
本节从 John Ousterhout 最近的著作“软件设计哲学”[9]中汲取灵感。该书的其中一章是“定义不存在的错误”。我们将尝试将此建议应用于 Go 语言。
让我们编写一个函数来计算文件中的行数。
1 | func CountLines(r io.Reader) (int, error) { |
由于我们遵循前面部分的建议,CountLines
需要一个 io.Reader
,而不是一个 *File
;它的任务是调用者为我们想要计算的内容提供 io.Reader
。
我们构造一个 bufio.Reader
,然后在一个循环中调用 ReadString
方法,递增计数器直到我们到达文件的末尾,然后我们返回读取的行数。
至少这是我们想要编写的代码,但是这个函数由于需要错误处理而变得更加复杂。 例如,有这样一个奇怪的结构:
1 | _, err = br.ReadString('\n') |
我们在检查错误之前增加了行数,这样做看起来很奇怪。
我们必须以这种方式编写它的原因是,如果在遇到换行符之前就读到文件结束,则 ReadString
将返回错误。如果文件中没有换行符,同样会出现这种情况。
为了解决这个问题,我们重新排列逻辑增来加行数,然后查看是否需要退出循环。
注意:
这个逻辑仍然不完美,你能发现错误吗?
但是我们还没有完成检查错误。当 ReadString
到达文件末尾时,预期它会返回 io.EOF
。ReadString
需要某种方式在没有什么可读时来停止。因此,在我们将错误返回给 CountLine
的调用者之前,我们需要检查错误是否是 io.EOF
,如果不是将其错误返回,否则我们返回 nil
说一切正常。
我认为这是 Russ Cox 观察到错误处理可能会模糊函数操作的一个很好的例子。我们来看一个改进的版本。
1 | func CountLines(r io.Reader) (int, error) { |
这个改进的版本从 bufio.Reader
切换到 bufio.Scanner
。
在 bufio.Scanner
内部使用 bufio.Reader
,但它添加了一个很好的抽象层,它有助于通过隐藏 CountLines
的操作来消除错误处理。
注意:
bufio.Scanner
可以扫描任何模式,但默认情况下它会查找换行符。
如果扫描程序匹配了一行文本并且没有遇到错误,则 sc.Scan()
方法返回 true
。因此,只有当扫描仪的缓冲区中有一行文本时,才会调用 for
循环的主体。这意味着我们修改后的 CountLines
正确处理没有换行符的情况,并且还处理文件为空的情况。
其次,当 sc.Scan
在遇到错误时返回 false
,我们的 for
循环将在到达文件结尾或遇到错误时退出。bufio.Scanner
类型会记住遇到的第一个错误,一旦我们使用 sc.Err()
方法退出循环,我们就可以获取该错误。
最后, sc.Err()
负责处理 io.EOF
并在达到文件末尾时将其转换为 nil
,而不会遇到其他错误。
贴士:
当遇到难以忍受的错误处理时,请尝试将某些操作提取到辅助程序类型中。
我的第二个例子受到了 Errors are values
博客文章[10]的启发。
在本章前面我们已经看过处理打开、写入和关闭文件的示例。错误处理是存在的,但是接收范围内的,因为操作可以封装在诸如 ioutil.ReadFile
和 ioutil.WriteFile
之类的辅助程序中。但是,在处理底层网络协议时,有必要使用 I/O
原始的错误处理来直接构建响应,这样就可能会变得重复。看一下构建 HTTP
响应的 HTTP
服务器的这个片段。
1 | type Header struct { |
首先,我们使用 fmt.Fprintf
构造状态码并检查错误。 然后对于每个标题,我们写入键值对,每次都检查错误。 最后,我们使用额外的 \r\n
终止标题部分,检查错误之后将响应主体复制到客户端。 最后,虽然我们不需要检查 io.Copy
中的错误,但我们需要将 io.Copy
返回的两个返回值形式转换为 WriteResponse
的单个返回值。
这里很多重复性的工作。 我们可以通过引入一个包装器类型 errWriter
来使其更容易。
errWriter
实现 io.Writer
接口,因此可用于包装现有的 io.Writer
。 errWriter
写入传递给其底层 writer
,直到检测到错误。 从此时起,它会丢弃任何写入并返回先前的错误。
1 | type errWriter struct { |
将 errWriter
应用于 WriteResponse
可以显着提高代码的清晰度。 每个操作不再需要自己做错误检查。 通过检查 ew.err
字段,将错误报告移动到函数末尾,从而避免转换从 io.Copy
的两个返回值。
最后,我想提一下你应该只处理错误一次。 处理错误意味着检查错误值并做出单一决定。
1 | // WriteAll writes the contents of buf to the supplied writer. |
如果你做出的决定少于一个,则忽略该错误。 正如我们在这里看到的那样, w.WriteAll
的错误被丢弃。
但是,针对单个错误做出多个决策也是有问题的。 以下是我经常遇到的代码。
1 | func WriteAll(w io.Writer, buf []byte) error { |
在此示例中,如果在 w.Write
期间发生错误,则会写入日志文件,注明错误发生的文件与行数,并且错误也会返回给调用者,调用者可能会记录该错误并将其返回到上一级,一直回到程序的顶部。
调用者可能正在做同样的事情
1 | func WriteConfig(w io.Writer, conf *Config) error { |
因此你在日志文件中得到一堆重复的内容,
1 | unable to write: io.EOF |
但在程序的顶部,虽然得到了原始错误,但没有相关内容。
1 | err := WriteConfig(f, &conf) |
我想深入研究这一点,因为作为个人偏好, 我并没有看到 logging
和返回的问题。
1 | func WriteConfig(w io.Writer, conf *Config) error { |
很多问题是程序员忘记从错误中返回。正如我们之前谈到的那样,Go 语言风格是使用 guard clauses
以及检查前提条件作为函数进展并提前返回。
在这个例子中,作者检查了错误,记录了它,但忘了返回。这就引起了一个微妙的错误。
Go 语言中的错误处理规定,如果出现错误,你不能对其他返回值的内容做出任何假设。由于 JSON
解析失败,buf
的内容未知,可能它什么都没有,但更糟的是它可能包含解析的 JSON
片段部分。
由于程序员在检查并记录错误后忘记返回,因此损坏的缓冲区将传递给 WriteAll
,这可能会成功,因此配置文件将被错误地写入。但是,该函数会正常返回,并且发生问题的唯一日志行是有关 JSON
解析错误,而与写入配置失败有关。
发生错误的原因是作者试图在错误消息中添加 context
。 他们试图给自己留下一些线索,指出错误的根源。
让我们看看使用 fmt.Errorf
的另一种方式。
1 | func WriteConfig(w io.Writer, conf *Config) error { |
通过将注释与返回的错误组合起来,就更难以忘记错误的返回来避免意外继续。
如果写入文件时发生 I/O
错误,则 error
的 Error()
方法会报告以下类似的内容;
1 | could not write config: write failed: input/output error |
github.com/pkg/errors
包装 errors
fmt.Errorf
模式适用于注释错误 message
,但这样做的代价是模糊了原始错误的类型。 我认为将错误视为不透明值对于松散耦合的软件非常重要,因此如果你使用错误值做的唯一事情是原始错误的类型应该无关紧要的面孔
nil
。但是在某些情况下,我认为它们并不常见,您需要恢复原始错误。 在这种情况下,使用类似我的 errors
包来注释这样的错误, 如下
1 | func ReadFile(path string) ([]byte, error) { |
现在报告的错误就是 K&D
[11]样式错误,
1 | could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory |
并且错误值保留对原始原因的引用。
1 | func main() { |
因此,你可以恢复原始错误并打印堆栈跟踪;
1 | original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory |
使用 errors
包,你可以以人和机器都可检查的方式向错误值添加上下文。 如果昨天你来听我的演讲,你会知道这个库在被移植到即将发布的 Go 语言版本的标准库中。
由于 Go 语言的并发功能,经常被选作项目编程语言。 Go 语言团队已经竭尽全力以廉价(在硬件资源方面)和高性能来实现并发,但是 Go 语言的并发功能也可以被用来编写性能不高同时也不太可靠的代码。在结尾,我想留下一些建议,以避免 Go 语言的并发功能带来的一些陷阱。
Go 语言以 channels
以及 select
和 go
语句来支持并发。如果你已经从书籍或培训课程中正式学习了 Go 语言,你可能已经注意到并发部分始终是这些课程的最后一部分。这个研讨会也没有什么不同,我选择最后覆盖并发,好像它是 Go 程序员应该掌握的常规技能的额外补充。
这里有一个二分法; Go 语言的最大特点是简单、轻量级的并发模型。作为一种产品,我们的语言几乎只推广这个功能。另一方面,有一种说法认为并发使用起来实际上并不容易,否则作者不会把它作为他们书中的最后一章,我们也不会遗憾地来回顾其形成过程。
本节讨论了 Go 语言的并发功能的“坑”。
这个程序有什么问题?
1 | package main |
该程序实现了我们的预期,它提供简单的 Web 服务。 然而,它同时也做了其他事情,它在无限循环中浪费 CPU 资源。 这是因为 main
的最后一行上的 for {}
将阻塞 main goroutine
,因为它不执行任何 IO、等待锁定、发送或接收通道数据或以其他方式与调度器通信。
由于 Go 语言运行时主要是协同调度,该程序将在单个 CPU 上做无效地旋转,并可能最终实时锁定。
我们如何解决这个问题? 这是一个建议。
1 | package main |
这看起来很愚蠢,但这是我看过的一种常见解决方案。 这是不了解潜在问题的症状。
现在,如果你有更多的经验,你可能会写这样的东西。
1 | package main |
空的 select
语句将永远阻塞。 这是一个有用的属性,因为现在我们不再调用 runtime.GoSched()
而耗费整个 CPU。 但是这也只是治疗了症状,而不是病根。
我想向你提出另一种你可能在用的解决方案。 与其在 goroutine
中运行 http.ListenAndServe
,会给我们留下处理 main goroutine
的问题,不如在 main goroutine
本身上运行 http.ListenAndServe
。
贴士:
如果 Go 语言程序的main.main
函数返回,无论程序在一段时间内启动的其他goroutine
在做什么, Go 语言程序会无条件地退出。
1 | package main |
所以这是我的第一条建议:如果你的 goroutine
在得到另一个结果之前无法取得进展,那么让自己完成此工作而不是委托给其他 goroutine
会更简单。
这通常会消除将结果从 goroutine
返回到其启动程序所需的大量状态跟踪和通道操作。
贴士:
许多 Go 程序员过度使用goroutine
,特别是刚开始时。与生活中的所有事情一样,适度是成功的关键。
以下两个 API 有什么区别?
1 | // ListDirectory returns the contents of dir. |
1 | // ListDirectory returns a channel over which |
首先,最明显的不同: 第一个示例将目录读入切片然后返回整个切片,如果出错则返回错误。这是同步发生的,ListDirectory
的调用者会阻塞,直到读取了所有目录条目。根据目录的大小,这可能需要很长时间,并且可能会分配大量内存来构建目录条目。
让我们看看第二个例子。 这个示例更像是 Go 语言风格,ListDirectory
返回一个通道,通过该通道传递目录条目。当通道关闭时,表明没有更多目录条目。由于在 ListDirectory
返回后发生了通道的填充,ListDirectory
可能会启动一个 goroutine
来填充通道。
注意:
第二个版本实际上不必使用 Go 协程; 它可以分配一个足以保存所有目录条目而不阻塞的通道,填充通道,关闭它,然后将通道返回给调用者。但这样做不太现实,因为会消耗大量内存来缓冲通道中的所有结果。
通道版本的 ListDirectory
还有两个问题:
ListDirectory
无法告诉调用者通过通道返回的项目集是否完整。调用者无法区分空目录和读取目录的错误。两者都导致从 ListDirectory
返回的通道立即关闭。ListDirectory
使用的严重限制,即使可能已经收到了它想要的答案,调用者也必须花时间从通道中读取。就中型到大型目录的内存使用而言,它可能更有效,但这种方法并不比原始的基于切片的方法快。以上两种实现所带来的问题的解决方案是使用回调,该回调是在执行时在每个目录条目的上下文中调用函数。
1 | func ListDirectory(dir string, fn func(string)) |
毫不奇怪,这就是 filepath.WalkDir
函数的工作方式。
贴士:
如果你的函数启动了goroutine
,你必须为调用者提供一种明确停止goroutine
的方法。 把异步执行函数的决定留给该函数的调用者通常会更容易些。
前面的例子显示当一个任务时没有必要时使用 goroutine
。但使用 Go 语言的原因之一是该语言提供的并发功能。实际上,很多情况下你希望利用硬件中可用的并行性。为此,你必须使用 goroutines
。
这个简单的应用程序在两个不同的端口上提供 http
服务,端口 8080
用于应用程序服务,端口 8001
用于访问 /debug/pprof
终端。
1 | package main |
虽然这个程序不是很复杂,但它代表了真实应用程序的基础。
该应用程序存在一些问题,因为它随着应用程序的增长而显露出来,所以我们现在来解决其中的一些问题。
1 | func serveApp() { |
通过将 serveApp
和 serveDebug
处理程序分解成为它们自己的函数,我们将它们与 main.main
分离。 也遵循了上面的建议,并确保 serveApp
和 serveDebug
将它们的并发性留给调用者。
但是这个程序存在一些可操作性问题。 如果 serveApp
返回,那么 main.main
将返回,导致程序关闭并由你使用的进程管理器来重新启动。
贴士:
正如 Go 语言中的函数将并发性留给调用者一样,应用程序应该将监视其状态和检测是否重启的工作留给另外的程序来做。 不要让你的应用程序负责重新启动自己,最好从应用程序外部处理该过程。
然而,serveDebug
是在一个单独的 goroutine
中运行的,返回后该 goroutine
将退出,而程序的其余部分继续。 由于 /debug
处理程序已停止工作很久,因此操作人员不会很高兴发现他们无法在你的应用程序中获取统计信息。
我们想要确保的是,如果任何负责提供此应用程序的 goroutine
停止,我们将关闭该应用程序。
1 | func serveApp() { |
现在 serverApp
和 serveDebug
检查从 ListenAndServe
返回的错误,并在需要时调用 log.Fatal
。因为两个处理程序都在 goroutine
中运行,所以我们将 main goroutine
停在 select{}
中。
这种方法存在许多问题:
ListenAndServer
返回 nil
错误,则不会调用 log.Fatal
,并且该端口上的 HTTP 服务将在不停止应用程序的情况下关闭。log.Fatal
调用 os.Exit
,它将无条件地退出程序; defer
不会被调用,其他 goroutines
也不会被通知关闭,程序就停止了。 这使得编写这些函数的测试变得困难。贴士:
只在main.main
或init
函数中的使用log.Fatal
。
我们真正想要的是任何错误发送回 goroutine
的调用者,以便它可以知道 goroutine
停止的原因,可以干净地关闭程序进程。
1 | func serveApp() error { |
我们可以使用通道来收集 goroutine
的返回状态。通道的大小等于我们想要管理的 goroutine
的数量,这样发送到 done
通道就不会阻塞,因为这会阻止 goroutine
的关闭,导致它泄漏。
由于没有办法安全地关闭 done
通道,我们不能使用 for range
来循环通道直到获取所有 goroutine
发来的报告,而是循环我们开启的多个 goroutine
,即通道的容量。
现在我们有办法等待每个 goroutine
干净地退出并记录他们遇到的错误。所需要的只是一种从第一个 goroutine
转发关闭信号到其他 goroutine
的方法。
事实证明,要求 http.Server
关闭是有点牵扯的,所以我将这个逻辑转给辅助函数。serve
助手使用一个地址和 http.Handler
,类似于 http.ListenAndServe
,还有一个 stop
通道,我们用它来触发 Shutdown
方法。
1 | func serve(addr string, handler http.Handler, stop <-chan struct{}) error { |
现在,每次我们在 done
通道上收到一个值时,我们关闭 stop
通道,这会导致在该通道上等待的所有 goroutine
关闭其 http.Server
。 这反过来将导致其余所有的 ListenAndServe
goroutines
返回。 一旦我们开启的所有 goroutine
都停止了,main.main
就会返回并且进程会干净地停止。
贴士:
自己编写这种逻辑是重复而微妙的。 参考下这个包: https://github.com/heptio/workgroup,它会为你完成大部分工作。
**引用: **
1. https://gaston.life/books/effective-programming/
2. https://talks.golang.org/2014/names.slide#4
3. https://www.infoq.com/articles/API-Design-Joshua-Bloch
1. https://www.lysator.liu.se/c/pikestyle.html
2. https://speakerdeck.com/campoy/understanding-nil
3. https://www.youtube.com/watch?v=Ic2y6w8lMPA
4. https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88
5. https://golang.org/doc/go1.4#internalpackages
6. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
7. https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
8. https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
9. https://www.amazon.com/Philosophy-Software-Design-John-Ousterhout/dp/1732102201
原文链接:Practical Go: Real world advice for writing maintainable Go programs
ctrl+s
多次保存会生成多篇文章的问题1、基于人口的统计学推荐(Demographic-based Recommendation)
2、基于内容的推荐(Content-based Recommendation)
3、基于协同过滤的推荐(Collaborative Filtering-based Recommendation)
根据物品或内容的元数据,发现物品或内容的相关性,然后基于用户以前的喜好记录推荐给用户相似的物品,如图所示:
上图给出了基于内容推荐的一个典型的例子,电影推荐系统,首先我们需要对电影的元数据有一个建模,这里只简单的描述了一下电影的类型;然后通过电影的元数据发现电影间的相似度,因为类型都是“爱情,浪漫”电影 A 和 C 被认为是相似的电影(当然,只根据类型是不够的,要得到更好的推荐,我们还可以考虑电影的导演,演员等等);最后实现推荐,对于用户 A,他喜欢看电影 A,那么系统就可以给他推荐类似的电影 C。
(1)基于用户的协同过滤推荐(User-based Collaborative Filtering Recommendation)
基于用户的协同过滤推荐算法先使用统计技术寻找与目标用户有相同喜好的邻居,然后根据目标用户的邻居的喜好产生向目标用户的推荐。基本原理就是利用用户访问行为的相似性来互相推荐用户可能感兴趣的资源,如图所示:
上图示意出基于用户的协同过滤推荐机制的基本原理,假设用户 A 喜欢物品 A、物品 C,用户 B 喜欢物品 B,用户 C 喜欢物品 A 、物品 C 和物品 D;从这些用户的历史喜好信息中,我们可以发现用户 A 和用户 C 的口味和偏好是比较类似的,同时用户 C 还喜欢物品 D,那么我们可以推断用户 A 可能也喜欢物品 D,因此可以将物品 D 推荐给用户 A。
(2)基于项目的协同过滤推荐(Item-based Collaborative Filtering Recommendation)
根据所有用户对物品或者信息的评价,发现物品和物品之间的相似度,然后根据用户的历史偏好信息将类似的物品推荐给该用户,如图所示:
上图表明基于项目的协同过滤推荐的基本原理,用户A喜欢物品A和物品C,用户B喜欢物品A、物品B和物品C,用户C喜欢物品A,从这些用户的历史喜好中可以认为物品A与物品C比较类似,喜欢物品A的都喜欢物品C,基于这个判断用户C可能也喜欢物品C,所以推荐系统将物品C推荐给用户C。
(3)基于模型的协同过滤推荐(Model-based Collaborative Filtering Recommendation)
基模型的协同过滤推荐就是基于样本的用户喜好信息,训练一个推荐模型,然后根据实时的用户喜好的信息进行预测推荐。
基于内容的推荐只考虑了对象的本身性质,将对象按标签形成集合,如果你消费集合中的一个则向你推荐集合中的其他对象;
基于协同过滤的推荐算法,充分利用集体智慧,即在大量的人群的行为和数据中收集答案,以帮助我们对整个人群得到统计意义上的结论,推荐的个性化程度高,基于以下两个出发点:(1)兴趣相近的用户可能会对同样的东西感兴趣;(2)用户可能较偏爱与其已购买的东西相类似的商品。也就是说考虑进了用户的历史习惯,对象客观上不一定相似,但由于人的行为可以认为其主观上是相似的,就可以产生推荐了。
以上答案只是参考IBM官网资料探索推荐引擎内部的秘密,第 1 部分: 推荐引擎初探,然后结合其他资料理解总结的,如有更好意见谢谢分享。
Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。
Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
下面我们来看一个例子,在线演示:https://play.golang.org/p/U9U-qjuY0t1
1 | package main |
执行结果说明fuck函数中的sleep三秒并没有影响喜特
的输出。
如果说 goroutine 是Go语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
下面我们利用goroutine+channel来实现一个生产消费者模型,示例代码如下,在线执行:https://play.golang.org/p/lqUBugLdU-I
1 | package main |
Java中值类型和引用类型都是定死的,int、double、float、long、byte、short、char、boolean为值类型,其他的都是引用类型,而Go语言中却不是这样。
在Go语言中:
&
表示取地址,例如你有一个变量a
那么&a
就是变量a
在内存中的地址,对于Golang指针也是有类型的,比如a是一个string那么&a是一个string的指针类型,在Go里面叫&string。*
表示取值,接上面的例子,假设你定义b := &a
如果你打印b
,那么输出的是&a
的内存地址,如果要取值,那么需要使用:*b
下面我们来看下例子,在线运行:https://play.golang.org/p/jxAKyVMjnoy
1 | package main |
Go 语言(或 Golang)起源于 2007 年,并在 2009 年正式对外发布。Go 是非常年轻的一门语言,它的主要目标是“兼具Python等动态语言的开发速度和 C/C++ 等编译型语言的性能与安全性”。
数据类型 | 说明 |
---|---|
bool | 布尔 |
string | 字符串 |
int | uint8,uint16,uint32,uint64,int8,int16,int32,int64 |
float | float32,float64 |
byte | byte |
参考:https://www.runoob.com/go/go-data-types.html
在线运行示例:https://play.golang.org/p/-4RylAqUV36
1 | package main |
我们来执行一下:
1 | go run main.go # main.go 为刚刚创建的那个文件的名称 |
在线运行示例:https://play.golang.org/p/zPqCkRZgrgp
1 | package main |
在线运行示例:https://play.golang.org/p/0My8veBvtJ8
1 | package main |
try cache
、throw
,Go语言是通过将error
作为返回值来处理异常。下面我们通过一个示例来了解一下,在线运行示例:https://play.golang.org/p/PYy3ueuPFS6
1 | package main |
该示例输出结果为:
1 | execute func log1 |
但函数有多个返回值的时候,有时你只关注其中一个返回值,这种情况下你可以将其他的返回值赋值给空白符:_
,如下:
1 | _, err := Add2(1, 2) |
空白符特殊在于实际上返回值并没有赋值,所以你可以随意将不同类型的值赋值给他,而不会由于类型不同而报错。
Go语言不是像Java那样的面向对象的语言,他没有对象和继承的概念。也没有class
的概念。在Go语言中有个概念叫做结构体(struct
),结构体和Java中的class
比较类似。下面我们定义一个结构体:
1 | type User struct { |
上面我们定义了一个结构体User
,并为该结构体分别设置了三个公有属性:Name/Gender/Age,下面我们来创建一个User对象。
1 | user := User{ |
结构体的属性可以在结构体内直接声明,那么如何为结构体声明函数(即Java中的方法)呢,我们来看下下面的示例:在线运行示例:https://play.golang.org/p/01_cTu0RzdH
1 | package main |
Java中值类型和引用类型都是定死的,int、double、float、long、byte、short、char、boolean为值类型,其他的都是引用类型,而Go语言中却不是这样。
在Go语言中:
&
表示取地址,例如你有一个变量a
那么&a
就是变量a
在内存中的地址,对于Golang指针也是有类型的,比如a是一个string那么&a是一个string的指针类型,在Go里面叫&string。*
表示取值,接上面的例子,假设你定义b := &a
如果你打印b
,那么输出的是&a
的内存地址,如果要取值,那么需要使用:*b
下面我们来看下例子,在线运行:https://play.golang.org/p/jxAKyVMjnoy
1 | package main |
Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。
Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。
下面我们来看一个例子(在线演示:https://play.golang.org/p/U9U-qjuY0t1)
1 | package main |
执行结果说明fuck函数中的sleep三秒并没有影响喜特
的输出。
如果说 goroutine 是Go语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。
下面我们利用goroutine+channel来实现一个生产消费者模型,示例代码如下:(在线执行:https://play.golang.org/p/lqUBugLdU-I)
1 | package main |
tag
)删除本地tag很简单,直接:git tag -d tagname
但是我们这样删除之后远程标签其实并未删除,通过以下方式可以删除远程标签。
1 | 假设标签名称为:v1.0.0 |
我有两个Github账号,在配置sshkey的时候是会提示Key is already in use
。因为github无法将相同的sshkey配置到不同的账号下,那么就要考虑同一台机器如何配置两个sshkey了。
因为之前已经存在~/.ssh/id_rsa
文件,所以这次生成的时候我们指定输出的文件名为id_rsa2
1 | ssh-keygen -t rsa -C "example@example.com" -f ~/.ssh/id_rsa2 |
在~/.ssh/
目录下创建config配置文件,内容如下,里面有详细的解释。
1 | # 原有的配置 |
假如我们的仓库地址为:git@github.com:name/project.git
,那么按照上面config
文件中Host
的配置,需要将仓库地址修改为:git@github_2.com:name/project.git
,修改git沧湖远程url的命令如下:
1 | git remote set-url origin git@github_2.com:name/project.git |
之前一直使用的搬瓦工的vps搭建的shadowsocks,但是最近被封掉了。正好手里还有一台阿里云香港服务器,所以就自己搭建了一个,下面是搭建流程。
我是使用shadowsocks-go
进行搭建的,是基于golang编写的,所以先需要安装golang环境,请自行安装。
shadowsocks-go项目地址:https://github.com/shadowsocks/shadowsocks-go
安装方式:
1 | go get github.com/shadowsocks/shadowsocks-go/cmd/shadowsocks-server |
安装完成之后请在:$GOPATH/bin
目录下找到:shadowsocks-server
文件。
然后在该文件同文件夹下创建配置文件:config.json
,密码端口请自行修改,我们这里使用的服务端端口为:8388
1 | { |
另外阿里云默认情况下8388端口是不对外开放的,请去阿里云控制到,找到对应的ecs实例,找到该实例的安全组,将8388
端口添加到白名单。
目标:将mariadb默认的datadir(/var/lib/mysql)迁移到
/data/mysql
1 | systemctl stop mariadb |
1 | mkdir /data/mysql |
1 | cp -a /var/lib/mysql /data/mysql |
1 | vi /etc/my.cnf |
1 | systemctl start mariadb |
安装命令
1 | yum -y install mariadb mariadb-server |
安装完成MariaDB,首先启动MariaDB
1 | systemctl start mariadb |
设置开机启动
1 | systemctl enable mariadb |
接下来进行MariaDB的相关简单配置
1 | mysql_secure_installation |
首先是设置密码,会提示先输入密码
1 | Enter current password for root (enter for none):<–初次运行直接回车 |
设置密码
1 | Set root password? [Y/n] <– 是否设置root用户密码,输入y并回车或直接回车 |
其他配置
1 | Remove anonymous users? [Y/n] <– 是否删除匿名用户,回车 |
初始化MariaDB完成,接下来测试登录
1 | mysql -uroot -ppassword |
完成。
产品最近需要花一个万年历的图片用于分享使用,找了Java的很多库都不好用,于是在其他万年历的网页中找到了下面的代码,然后用Java调用JavaScript将万年历计算出来。
1 |
|
1 | **粗体** |
1 | *斜体* |
1 | # 一级标题 # |
1 | *** |
1 | 上角标 x^2^ |
1 | ++下划线++ |
1 | ==标记== |
1 | > 一级 |
1 | 有序列表 |
1 | - [x] 已完成任务 |
1 | [链接](www.baidu.com) |
``` type
代码段落
```
` 代码块 `
1 | int main() |
code
1 | | 标题1 | 标题2 | 标题3 | |
标题1 | 标题2 | 标题3 |
---|---|---|
左对齐 | 居中 | 右对齐 |
———————- | ————- | —————– |
1 | hello[^hello] |
见底部脚注^hello
参考网站: https://www.webpagefx.com/tools/emoji-cheat-sheet/
1 | :laughing: |
:laughing::blush::smiley::)
我们可以渲染公式例如:$x_i + y_i = z_i$和$\sum_{i=1}^n a_i=0$
我们也可以单行渲染
$$\sum_{i=1}^n a_i=0$$
具体可参照katex文档和katex支持的函数以及latex文档
::: hljs-left::: hljs-left
居左
:::
:::
::: hljs-center::: hljs-center
居中
:::
:::
::: hljs-right::: hljs-right
居右
:::
:::
术语一
: 定义一
包含有行内标记的术语二
: 定义二
{一些定义二的文字或代码}
定义二的第三段
1 | 术语一 |
*[HTML]: Hyper Text Markup Language
*[W3C]: World Wide Web Consortium
HTML 规范由 W3C 维护
1 | *[HTML]: Hyper Text Markup Language |
本书中的内容使用 CC BY-NC-SA 4.0(署名 - 非商业性使用 - 相同方式共享4.0许可协议)授权。你不必为此书付费。
你可以免费的复制、发布、修改或者展示此书。但是,这本书的版权归原作者Karl Seguin所有,不要将此书用于商业目的。
关于许可证的全部内容你可以浏览以下网站:
http://creativecommons.org/licenses/by-nc-sa/4.0/
这本书的最新版本可以在以下网站获得:
http://github.com/karlseguin/the-little-go-book
每次提起学习一门新语言,我真的是又爱又恨。一方面,语言是我们的行事之本,即使一些小的变化都会对事情有重大的影响。可能有时一闪而过的 灵感 就会对你如何编程产生长久的影响力,并重新定义你对其他语言的期望。而头疼的是,语言的设计是呈增量式的,要学习新的关键字、类型系统、代码风格以及新的库、社区和范例真的是难言其苦。相比于所有其他必须学习的事情,花时间在一门新的语言上貌似真的是很糟糕的投资。
即便如此,我们还是得走下去。我们 必须 得乐于每天一点点地进步,因为“语言是我们的行事之本”。虽然语言的变化往往会是循序渐进的,但它影响范围仍然很广,包括了有生产率、可读性、性能、可测试性、依赖性管理、错误处理、文档、简要、社区、标准库等等。所以,有好点的说法来形容 千刀万剐 么?
留给我们一个重要问题就是:为什么选择 Go? 。对于我来说,有两条原因。第一条,它是一种相对简单的语言,且具有相对简单的标准库。在很多方面,Go 的特性语法是为了简化我们在过去几十年中添加到编程语言中的一些复杂特性。另外一条原因就是对于许多开发者来说,它将补充您的知识面。
Go 是作为系统语言(例如:操作系统,设备驱动程序)创建的,因此它针对的是 C 和 C++ 开发人员。按照 Go 团队的说法,应用程序开发人员已经成为 Go 的主要用户而不是系统开发人员了,这个说法我也是相信的。为什么?我不能权威的代表系统开发人员说话,但对于我们这些构建网站,服务,桌面应用程序等的人来说,它可以部分的归结为对一类系统的新兴需求,这类系统介于低级系统应用程序和更高级的应用程序之间。
可能 Go 语言有消息传递机制,缓存,重计算数据分析,命令行接口,体制和监控,我不知道给 Go 语言什么样的标签,但是在我的职业生涯中,随着系统的复杂性不断增加,以及动辄成千上万的并发,显然对定制基础类型系统的需求不断增加。你可以使用 Ruby 或者 Python 构建这样的系统(大多人都这样做),但这些类型的系统可以从更严格的类型系统和更高的性能中受益。类似地,你可以使用 Go 来构建网站(很多人都愿意这样做),但我仍然喜欢 Node 或者 Ruby 对这类系统展现出的表现力。
Go 语言还擅长其他领域。比如,当运行一个编译过的 Go 程序时,他没有依赖性。你不必担心用户是安装了 Ruby 或者 JVM,如果这样,你还要考虑版本。出于这个原因,Go 作为命令行程序以及其他并发类型的工具(日志收集器)的开发语言变得越来越流行。
坦白来说,学习 Go 可以有效利用你的时间。你不必担心会花费很长时间学习 Go 甚至掌握它,你最终会从你的努力中得到一些实用的东西。
对于写这本书我犹豫再三,主要有两个原因。第一个是 Go 有自己的文档,特别是 Effective Go。
另一个是在写一本关于语言类的书的时候我会有点不安。当我们写 《 The Little MongoDB Book》 这本书的时候,我完全假设大多数读者已经理解了关系型数据库和建模的基本知识。在写 《The Little Redis Book》这本书的时候,你也可以同样假设读者已经熟悉键值存储。
在我考虑未来的某些章节的时候,我知道不能再做出同样的假设。你花多长时间学习并理解接口,这是个新的概念,我希望你从中学到的不仅仅是 Go 有提供接口,并且还有如何使用它们。最终,我希望你向我反馈本书的哪部分讲得太细或者太粗,我会感到很欣慰,也算是我对读者们的小小要求了。
如果你想去尝试运行 Go 的代码,你可以去看看 Go Playground ,它可以在线运行你的代码并且不要安装任何东西。这也是你在 Go 的论坛区和 StackOverflow 等地方寻求帮助时分享 Go 代码的最常用方法。
Go 的安装很简单。你可以用源码去安装,但是我还是建议你使用其中一个预编译的二进制文件。当你 跳转到下载页面,你将会看到 Go 语言的在各个平台上的安装包。我们会避免这些东西并且学会如何在自己的平台上安装好 Go。正如你所看到的的那样,安装 Go 并不是很难。
除了一些简单的例子, Go 被设计成代码在工作区内运行。工作区是一个文件夹,这个文件夹由 bin
,pkg
,以及src
子文件夹组成的。你可能会试图强迫 Go 遵循自己的风格-不要这么去做。
一般,我把我的项目放在 ~/code
文件夹下。比如,~/code/blog
目录就包含了我的 blog 项目。对于 Go 来说,我的工作区域就是 ~/code/go
,然后我的 Go 写的项目代码就在 ~/code/go/src/blog
文件夹下。
简单来说,无论你希望把你的项目放在哪里,你最好创建一个 go
的文件夹,再在里面创建一个 src
的子文件夹。
下载适合你自己电脑系统的 tar.gz
文件。对于 OSX 系统来说,你可能会对 go#.#.#.darwin-amd64-osx10.8.tar.gz
感兴趣,其中 #.#.#
代表 Go 的最新版本号。
通过 tar -C /usr/local -xzf go#.#.#.darwin-amd64-osx10.8.tar.gz
命令将文件加压缩到 /usr/local
目录下
设置两个环境变量:
1.GOPATH
指向的是你的工作目录,对我来说,那个目录就是 $HOME/code/go
2.我们需要将 Go 的二进制文件添加到的 PATH
变量中。
你可以通过下面的 shell 去设置这两个环境变量:
echo 'export GOPATH=$HOME/code/go' >> $HOME/.profile
echo 'export PATH=$PATH:/usr/local/go/bin' >> $HOME/.profile
你需要将这些环境变量激活。你可以关掉 shell 终端,然后在打开 shell 终端,或者你可以在 shell 终端运行 source $HOME/.profile
。
在命令终端输入 go version
,你将会得到一个 go version go1.3.3 darwin / amd64
的输出,Go 就安装完成了。
下载最新的 zip 文件。如果你的电脑是 64 位的系统,你将需要 go#.#.#.windows-amd64.zip
,这里的 #.#.#
是 Go 的最新版本号。
解压缩 go#.#.#.windows-amd64.zip
文件到你选择的位置。 c:\Go
这个位置是个不错的选择。
在系统中设置两个环境变量:
GOPATH
同样的指向的是你的工作目录。这个变量看起来像c:\users\goku\work\go
这个样子。c:\Go\bin
到系统的 PATH
环境变量。你可以通过「系统」 控制面板的 「高级」 选项卡上的 「环境变量」按钮设置环境变量。 某些版本的 Windows 通过「系统」控制面板中的「高级系统选项」选项此控制面板。
打开一个 cmd
命令终端,输入 go version
。 你会得到一个 go version go1.3.3 windows/amd64
的输出,即表示 Go 安装完成。
Go 是一门编译型,具有静态类型和类 C 语言语法的语言,并且有垃圾回收(GC)机制。这是什么意思?
编译是将源代码翻译为更加低级的语言的过程——翻译成汇编语言(例如 Go),或是翻译成其他中间语言(如 Java 和 C#)。
编译型语言可能会让你很不爽,因为编译过程实在是太慢了。如果每次都需要花好几分钟甚至好几个小时去等待代码编译的话,很难进行快速迭代。而编译速度是 Go 的主要优化目标之一。这对我们这些从事大型项目开发或者是习惯用解释型快速看到程序结果的人来说,确实是一件好事。
编译型语言注重于运行速度和无依赖执行程序(至少对于 C/C++ 和 Go 来说是这样的,直接将依赖编译到程序中)。
静态类型意味着变量必须是特定的类型(如:int, string, bool, []byte 等等),这可以通过在声明变量的时候,指定变量的类型来实现,或者让编译器自行推断变量的类型(我们将很快可以看到实例)。
关于静态类型的东西,可以说的还有很多,但是我相信通过看代码能更好的理解静态类型是什么。如果你习惯于动态类型语言, 你可能会发现这很麻烦。这种想法没错,但是静态类型语言也有优点,特别是当你将静态类型和编译配对使用时。这两者经常混为一谈。确实当你有其中一个的时候,通常也会有另一个,但是这不是硬性规定的。使用强类型系统,编译器能够检测除语法错误之外的问题从而进一步优化。
当说到一门语言是类 C 语法的时候,通常意味着如果你用过其他类 C 语言如:C,C++,Java,JavaScript 和 C#,你会觉得 Go 的语法很熟悉——最少表面上是这样的。举个例子,&&
用于逻辑 AND,==
用于判断是否相等,{
和 }
是块的开始和结束,数组下标的起始值为 0。
类 C 语法也倾向于用分号表示作为语句结束符,并将条件写在括号中。Go 不支持这些,但是仍然使用括号来控制优先级。例如,一个 if
语句是这样的:
1 | if name == "Leto" { |
在很多复杂系统中,括号符还是很有用的:
1 | if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000) { |
除此之外,Go 要比 C# 或 Java 更接近 C - 不仅是语法方面,还有目的方面。这反映在语言的简洁和简单上,希望你在学习它的时候能慢慢体会这一点。
如果我们用 x = 4 来申明和赋值变量,那么我们就可以同时开始和结束对变量的查看了。遗憾的是,Go 更为复杂些。我们将通过简单的示例来开始我们的学习。然后,在下一章中,我们会在创建和使用结构体时,进一步扩展。尽管如此,你可能还得花一段时间来适应,才能感受到它带给你的舒适感。
你可能会想:“哇!这有什么复杂的?”。 让我们开始一个例子。
下面的例子是 Go 中,申明变量和赋值最为明确的方法,但也是最为冗长的方法:
1 | package main |
这里我们定义了一个 int
类型的变量 power
。默认情况下,Go 会为变量分配默认值。Integers 的默认值是 0
,booleans 默认值是 false
,strings 默认值是 ""
等等。下面,我们创建一个值为 9000
的名为 power
的变量。我们可以将定义和赋值两行代码合并起来:
1 | var power int = 9000 |
不过,这么写太长了。Go 提供了一个方便的短变量声明运算符 :=
,它可以自动推断变量类型:
1 | power := 9000 |
这非常方便,它可以跟函数结合使用,就像这样:
1 | func main() { |
值得注意的是要用 :=
来声明变量以及给变量赋值。相同变量不能被声明两次(在相同作用域下),如果你尝试这样,会收到错误提示。
1 | func main() { |
编辑器会告诉你 * := 左侧不是新的变量*。这就意味着当我们首次声明一个变量时应该使用 :=
,后面再给变量赋值时应该使用 =
。这似乎很有道理,但是凭空来记忆且需要根据情况来切换却是很难的事。
如果你仔细阅读代码的错误信息,你会发现 variables 单词是个复数,即有多个变量,那是因为go支持多个变量同时赋值(使用 =
或者 :=
):
1 | func main() { |
另外,多个变量赋值的时候,只要其中有一个变量是新的,就可以使用:=
。例如:
1 | func main() { |
尽管变量 power
使用了两次:=
,但是编译器不会在第 2 次使用 :=
时报错,因为这里有一个新的 name
变量,它可以使用:=
。然后你不能改变 power
变量的类型,它已经被声明成一个整型,所以只能赋值整数。
到目前为止,你最后需要了解的一件事是,Go 会像 import 一样,不允许你在程序中拥有未使用的变量。例如:
1 | func main() { |
这将不会通过编译,因为 name
是一个被申明但是未被使用的变量,就像 import 的包未被使用时,也将会导致编译失败,但总的来说,我认为这有助于提高代码的清洁度和可读性。
还有更多关于的申明和赋值的技巧。初始化一个变量时,请使用: var NAME TYPE
;给变量申明及赋值时,请使用: NAME := VALUE
; 给之前已经申明过的变量赋值时,请使用: NAME = VALUE
一些变量,在创建的时候,就拥有一个简单定义的生命周期。对于函数中的变量,会在函数执行完后进行销毁。在别的语言中,对于编译器而言,这不会很明显。例如:函数返回的变量,或者由其他变量和对象所调用的变量,它们的生命周期是很难确定的。 如果没有垃圾回收机制,那么开发人员就得知道有哪些不需要用到的变量,并将它们释放。就像 C 语言,你需要使用 free(str);
来释放变量。
语言的垃圾回收机制(像:Ruby, Python, Java, JavaScript, C# , Go)是会对变量进行跟踪,并在没有使用它们的时候,进行释放。垃圾回收会增加一些额外的开销,但是也减少了一些致命性的 BUG。
创建一个简单的程序然后学习如何编译和运行它。打开你的文本编辑器写入下面的代码:
1 | package main |
保存文件并命名为 main.go
。 你可以将文件保存在任何地方;不必将这些琐碎的例子放在 go 的工作空间内。
接下来,打开一个 shell 或者终端提示符,进入到文件保存的目录内, 对于我而言, 应该输入 cd ~/code
进入到文件保存目录。
最后,通过敲入以下命令来运行程序:
1 | go run main.go |
如果一切正常(即你的 golang 环境配置的正确),你将看到 it’s over 9000! 。
但是编译步骤是怎么样的呢? go run
命令已经包含了编译和运行。它使用一个临时目录来构建程序,执行完然后清理掉临时目录。你可以执行以下命令来查看临时文件的位置:
1 | go run --work main.go |
明确要编译代码的话,使用 go build
:
1 | go build main.go |
这将产生一个可执行文件,名为 main
,你可以执行该文件。如果是在 Linux / OSX 系统中,别忘了使用 ./
前缀来执行,也就是输入 ./main
。
在开发中,你既可以使用 go run
也可以使用 go build
。但当你正式部署代码的时候,你应该部署通过 go build
产生的二进制文件并且执行它。
希望刚才执行的代码是可以理解的。我们刚刚创建了一个函数,并且使用内置函数 println
打印出了字符串。难道仅因为这里只有一个选择,所以 go run
知道执行什么吗??不。在 go 中程序入口必须是 main
函数,并且在 main
包内。
我们将在后面的章节中详细介绍包
。目前,我们将专注于理解 go 基础,一直会在 main
包中写代码。
如果你想尝试,你可以修改代码并且可以更改包名。使用 go run
执行程序将出现一个错误。 接着你可以将包名改回 main
,换一个不同的方法名,你会看到一个不同的错误。尝试使用 go build
代替 go run
来执行刚才的代码,注意代码编译时,没有入口点可以执行。但当你构建一个库时,确实完全正确的。
Go 有很多内建函数,例如 println
,可以在没有引用情况下直接使用。但是,如果不使用 Go 的标准库直接使用第三方库,我们就无法走的更远。import
关键字被用于去声明文件中代码要使用的包。
修改下我们的程序:
1 | package main |
你可以这样运行:
1 | go run main.go 9000 |
我们现在用了 Go 的两个标准包:fmt
和 os
。我们也介绍了另一个内建函数 len
。len
返回字符串的长度,字典值的数量,或者我们这里看到的,它返回了数组元素的数量。如果你想知道我们这里为什么期望得到两个参数,它是因为第一个参数 – 索引0处 – 总是当前可运行程序的路径。(更改程序将它打印出来亲自看看就知道了)
你可能注意到了,我们在函数名称前加上了前缀包名,例如,fmt.PrintLn
。这是不同于其他很多语言的。后续的章节中我们将学习到更多关于包的知识。现在,知道如何导入以及使用一个包就是一个好的开始。
在 Go 中,关于导包是很严格的。如果你导入了一个包却没有使用将会导致编译不通过。尝试运行下面的程序:
1 | package main |
你应该会得到两个关于 fmt
和 os
被导入却没有被使用的错误。这很烦人的是不是呀?绝对是这样的,不过随着时间的推移,你将慢慢习惯它(虽然仍然烦人,不过要以 Go 的思维写 Go)。Go 如此严格是因为没用的导入会降低编译速度;诚然,我们大多数人不会关注这个问题。
另一个需要记住的事情是 Go 的标准库已经有了很好的文档。你可以访问 https://golang.org/pkg/fmt/#Println 去看更多关于 PrintLn
函数的信息。你可以点击那个部分的头去看源代码。另外,也可以滚动到顶部查看关于 Go 格式化功能的更多消息。
如果没有互联网,你可以这样在本地获取文档:
1 | godoc -http=:6060 |
然后浏览器中访问 http://localhost:6060
这是个好时机指出函数是可以返回多个值的。让我们看三个函数:一个没有返回值,一个有一个返回值,一个有两个返回值。
1 | func log(message string) { |
我们可以像这样使用最后一个:
1 | value, exists := power("goku") |
有时候,你仅仅关注其中一个返回值。这个情况下,你可以将其他的返回值赋值给空白符_
:
1 | _, exists := power("goku") |
这不仅仅是一个惯例。_
,空白标识符,特殊在于实际上返回值并没有赋值。这让你可以一遍又一遍地使用 _
而不用管它的类型。
最后,关于函数声明还有些要说的。如果参数有相同的类型,您可以用这样一个简洁的用法:
1 | func add(a, b int) int { |
返回多个值可能是你经常使用的,你也可能会频繁地使用 _
丢弃一个值。命名返回值和稍微冗长的参数声明不太常用。尽管如此,你将很快遇到他们,所以了解他们很重要。
我们之前看了很多小的独立片段,在这点上,可能会感到有点脱节。我们将慢慢地构建更大的例子,将这些小的片段组合在一起。
如果你之前用的是动态类型语言,那么类型和声明的复杂性看起来像是在倒退。我并没有不同意你,在某些系统中动态语言可能更加有效。
如果你来自静态类型的语言,你可能对 Go 感到满意。类型推断以及多值返回的设计非常棒?(尽管这不是 Go 独有)。希望随着我们了解更多,你将会慢慢爱上这干净简洁的语法。
Go 不是像 C ++,Java,Ruby和C#一样的面向对象的(OO)语言。它没有对象和继承的概念,也没有很多与面向对象相关的概念,例如多态和重载。
Go所具有的是结构体的概念,可以将一些方法和结构体关联。Go 还支持一种简单但有效的组合形式。 总的来说,它会使代码变的更简单,但在某一些场合,你会错过面向对象提供的一些特性。(值得指出的是,通过组合实现继承是一场古老的战斗呐喊,Go 是我用过的第一种坚定立场的语言,在这个问题上。)
虽然 Go 不会像你以前使用的面向对象语言一样,但是你会注意到结构的定义和类的定义之间有很多相似之处。下面的代码定义了一个简单的 Saiyan
结构体:
1 | type Saiyan struct { |
我们将看明白怎么往这个结构体添加一个方法,就像面向对象类,会有方法作为 它的一部分。在这之前,我们先要知道如何申明结构体。
当我们第一次看到变量和声明时,我们只看了内置类型,比如整数和字符串。既然现在我们要讨论结构,那么我们需要扩展讨论范围到指针。
创建结构的值的最简单的方式是:
1 | goku := Saiyan{ |
注意: 上述结构末尾的逗号 ,
是必需的。没有它的话,编译器就会报错。你将会喜欢上这种必需的一致性,尤其当你使用一个与这种风格相反的语言或格式的时候。
我们不必设置所有或哪怕一个字段。下面这些都是有效的:
1 | goku := Saiyan{} |
就像未赋值的变量其值默认为 0 一样,字段也是如此。
此外,你可以不写字段名,依赖字段顺序去初始化结构体 (但是为了可读性,你应该把字段名写清楚):
1 | goku := Saiyan{"Goku", 9000} |
以上所有的示例都是声明变量 goku
并赋值。
许多时候,我们并不想让一个变量直接关联到值,而是让它的值为一个指针,通过指针关联到值。一个指针就是内存中的一个地址;指针的值就是实际值的地址。这是间接地获取值的方式。形象地来说,指针和实际值的关系就相当于房子和指向该房子的方向之间的关系。
为什么我们想要一个指针指向值而不是直接包含该值呢?这归结为 Go 中传递参数到函数的方式:就像复制。知道了这个,尝试理解一下下面的代码呢?
1 | func main() { |
上面程序运行的结果是 9000,而不是 19000,。为什么?因为 Super
修改了原始值 goku
的复制版本,而不是它本身,所以,Super
中的修改并不影响上层调用者。现在为了达到你的期望,我们可以传递一个指针到函数中:
1 | func main() { |
这一次,我们修改了两处代码。第一个是使用了 &
操作符以获取值的地址(它就是 取地址 操作符)。然后,我们修改了 Super
参数期望的类型。它之前期望一个 Saiyan
类型,但是现在期望一个地址类型 *Saiyan
,这里 *X
意思是 指向类型 X 值的指针 。很显然类型 Saiyan
和 *Saiyan
是有关系的,但是他们是不同的类型。
这里注意到我们仍然传递了一个 goku
的值的副本给 Super
,但这时 goku
的值其实是一个地址。所以这个副本值也是一个与原值相等的地址,这就是我们间接传值的方式。想象一下,就像复制一个指向饭店的方向牌。你所拥有的是一个方向牌的副本,但是它仍然指向原来的饭店。
我们可以证实一下这是一个地址的副本,通过修改其指向的值(尽管这可能不是你真正想做的事情):
1 | func main() { |
上面的代码,又一次地输出 9000。就像许多语言表现的那样,包括 Ruby,Python, Java 和 C#,Go 以及部分的 C#,只是让这个事实变得更明显一些。
同样很明显的是,复制一个指针比复制一个复杂的结构的消耗小多了。在 64 位的机器上面,一个指针占据 64 bit 的空间。如果我们有一个包含很多字段的结构,创建它的副本将会是一个很昂贵的操作。指针的真正价值在于能够分享它所指向的值。我们是想让 Super
修改 goku
的副本还是修改共享的 goku
值本身呢?
所有这些并不是说你总应该使用指针。这章末尾,在我们见识了结构的更多功能以后,我们将重新检视 指针与值这个问题。
我们可以把一个方法关联在一个结构体上:
1 | type Saiyan struct { |
在上面的代码中,我们可以这么理解,*Saiyan
类型是 Super
方法的接受者。然后我们可以通过下面的代码去调用 Super
方法:
1 | goku := &Saiyan{"Goku", 9001} |
结构体没有构造器。但是,你可以创建一个返回所期望类型的实例的函数(类似于工厂):
1 | func NewSaiyan(name string, power int) *Saiyan { |
这种模式以错误的方式惹恼了很多开发人员。一方面,这里有一点轻微的语法变化;另一方面,它确实感觉有点不那么明显。
我们的工厂不必返回一个指针;下面的形式是完全有效的:
1 | func NewSaiyan(name string, power int) Saiyan { |
到目前为止的例子中,Saiyan
有两个字段 Name
和 Power
,其类型分别为 string
和 int
。字段可以是任何类型 – 包括其他结构体类型以及目前我们还没有提及的 array,maps,interfaces 和 functions 等类型。
例如,我们可以扩展 Saiyan
的定义:
1 | type Saiyan struct { |
然后我们通过下面的方式初始化:
1 | gohan := &Saiyan{ |
尽管缺少构造器,Go 语言却有一个内置的 new
函数,使用它来分配类型所需要的内存。 new(X)
的结果与 &X{}
相同。
1 | goku := new(Saiyan) |
如何使用取决于你,但是你会发现大多数人更偏爱后一种写法无论是否有字段需要初始化,因为这看起来更具可读性:
1 | goku := new(Saiyan) |
无论你选择哪一种,如果你遵循上述的工厂模式,就可以保护剩余的代码而不必知道或担心内存分配细节
Go 支持组合, 这是将一个结构包含进另一个结构的行为。在某些语言中,这种行为叫做 特质 或者 混合。 没有明确的组合机制的语言总是可以做到这一点。在 Java 中, 可以使用 继承 来扩展结构。但是在脚本中并没有这种选项, 混合将会被写成如下形式:
1 | public class Person { |
这可能会非常繁琐。Person
的每个方法都需要在 Saiyan
中重复。Go 避免了这种复杂性:
1 | type Person struct { |
Saiyan
结构体有一个 Person
类型的字段。由于我们没有显示地给它一个字段名,所以我们可以隐式地访问组合类型的字段和函数。然而,Go 编译器确实给了它一个字段,下面这样完全有效:
1 | goku := &Saiyan{ |
上面两个都打印 「Goku」。
组合比继承更好吗?许多人认为它是一种更好的组织代码的方式。当使用继承的时候,你的类和超类紧密耦合在一起,你最终专注于结构而不是行为。
当你写 Go 代码的时候,很自然就会去问自己 应该是值还是指向值的指针呢? 这儿有两个好消息,首先,无论我们讨论下面哪一项,答案都是一样的:
第二,如果你不确定,那就用指针咯。
正如我们已经看到的,传值是一个使数据不可变的好方法(函数中改变它不会反映到调用代码中)。有时,这是你想要的行为,但是通常情况下,不是这样的。
即使你不打算改变数据,也要考虑创建大型结构体副本的成本。相反,你可能有一些小的结构:
1 | type Point struct { |
这种情况下,复制结构的成本能够通过直接访问 X
和 Y
来抵消,而没有其它任何间接操作。
还有,这些案例都是很微妙的,除非你迭代成千上万个这样的指针,否则你不会注意到差异。
从实际的角度看,这章介绍了结构体,如何使一个结构体的实例成为函数的接收者,以及添加指针到现有的 Go 类型系统知识中。下面的章节将建立在我们已经了解了什么是结构体以及其内部工作原理之上。
至此,我们已经学了一部分简单的类型和结构。现在,让我们开始学习 Arrays (数组), Slices (切片) 和 Maps (映射) 吧。
如果你学过 Python , Ruby , Perl , JavaScript 或者 PHP (或者更多其它的语言),那么你肯定习惯 动态数组 编程啦。这些数组的长度可以在添加数据的时候自行调整的。在 Go 中,像其它大部分语言一样,数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的了:
1 | var scores [10]int |
上面的数组最多可以容纳 10 个元素,索引是从 scores[0]
到 scores[9]
。试图访问超过界限的索引系统将会抛出编译或运行时错误。
我们可以在初始化数组的时候指定值:
1 | scores := [4]int{9001, 9333, 212, 33} |
我们可以使用 len
函数来获取数组的长度。range
函数在遍历迭代的时候使用:
1 | for index, value := range scores { |
数组非常高效但是很死板。很多时候,我们在事前并不知道数组的长多是多少。针对这个情况,slices (切片) 出来了。
在Go语言中,我们很少直接使用数组。取而代之的是使用切片。切片是轻量的包含并表示数组的一部分的结构。 这里有几种创建切片的方式,我们来看看什么情况下使用它们。首先在数组的基础之上进行一点点变化:
1 | scores := []int{1,4,293,4,9} |
和数组申明不同的是,我们的切片没有在方括号中定义长度。为了理解两者的不同,我们来看看另一种使用make
来创建切片的方式:
1 | scores := make([]int, 10) |
我们使用 make
关键字代替 new
, 是因为创建一个切片不仅是只分配一段内存(这个是 new
关键字的功能)。具体来讲,我们必须要为一个底层数组分配一段内存,同时也要初始化这个切片。在上面的代码中,我们初始化了一个长度是 10 ,容量是 10 的切片。长度是切片的长度,容量是底层数组的长度。在使用 make
创建切片时,我们可以分别的指定切片的长度和容量:
1 | scores := make([]int, 0, 10) |
上面的代码创建了一个长度是 0 ,容量是 10 的切片。(如果你仔细观察的话,你会注意到 make
和 len
被重载了。Go 的一些特性没有暴露出来给开发者使用,这也许会让你感到沮丧。)
为了更好的理解切片的长度和容量之间的关系,我们来看下面的的例子:
1 | func main() { |
我们上面的这个例子不能运行,为什么呢?因为切片的长度是 0 。没错,底层数组可以放 10 个元素,但是我们需要显式的扩展切片,才能访问到底层数组的元素。一种扩展切片的方式是通过 append
的关键字来实现:
1 | func main() { |
但是那并没有改变原始代码的意图。追加一个值到长度为0的切片中将会设置第一个元素。无论什么原因,我们崩溃的代码想去设置索引为7的元素值。为了实现这个,我们可以重新切片:
1 | func main() { |
我们可以调整的切片大小最大范围是多少呢?达到它的容量,这个例子中,是10。你可能在想 这实际上并没有解决数组固定长度的问题。但是 append
是相当特别的。如果底层数组满了,它将创建一个更大的数组并且复制所有原切片中的值(这个就很像动态语言 PHP,Python,Ruby,JavaScript 的工作方式)。这就是为什么上面的例子中我们必须重新将 append
返回的值赋值给 scores
变量:append
可能在原有底层数组空间不足的情况下创建了新值。
如果我告诉你 Go 使用 2x 算法来增加数组长度,你猜下面将会打印什么?
1 | func main() { |
初始 scores
的容量是5。为了存储25个值,它必须扩展三次容量,分别是 10,20,最终是40。
最后一个例子,考虑这个:
1 | func main() { |
这里输出是 [0, 0, 0, 0, 0, 9332]
,可能你觉得是[9332, 0, 0, 0, 0]
?对一个用户而言,这可能逻辑上是正确的。然而,对于一个编译器,你告诉他的是追加一个值到一个已经有5个值的切片。
最终,这有四种方式初始化一个切片:
1 | names := []string{"leto", "jessica", "paul"} |
什么时候该用哪个呢?第一个不用过多解释。当你事先知道数组中的值的时候,你可以使用这个方式。
当你想要写入切片具体的索引时,第二个方法很有用,例如:
1 | func extractPowers(saiyans []*Saiyans) []int { |
第三个版本是指向空的切片,用于当元素数量未知时与 append
连接。
最后一个版本是让我们声明一个初始的容量。如果我们大概知道元素的数量将是很有用的。
即使当你知道大小的时候,append
也可以使用,取决于个人偏好:
1 | func extractPowers(saiyans []*Saiyans) []int { |
切片作为数组的包装是一个很强大的概念。许多语言有切片数组的概念。JavaScript 和 Ruby 数组都有一个 slice
方法。Ruby 中你可以使用 [START..END]
获取一个切片,或者 Python 中可以通过 [START:END]
实现。然而,在这些语言中,一个切片实际上是复制了原始值的新数组。如果我们使用 Ruby,下面这段代码的输出是什么呢?
1 | scores = [1,2,3,4,5] |
答案是 [1, 2, 3, 4, 5]
。那是因为 slice
是一个新数组,并且复制了原有的值。现在,考虑 Go 中的情况:
1 | scores := []int{1,2,3,4,5} |
输出是 [1, 2, 999, 4, 5]
。
这改变了你编码的方式。例如,许多函数采用一个位置参数。JavaScript 中,如果你想去找到字符串中前五个字符后面的第一个空格(当然,在Go中切片也可以用于字符串),我们会这样写:
1 | haystack = "the spice must flow"; |
在 Go 中,我们这样使用切片:
1 | strings.Index(haystack[5:], " ") |
我们可以从上面的例子中看到,[X:]
是 从 X 到结尾 的简写,然而 [:X]
是 从开始到 X 的简写。不像其他的语言,Go 不支持负数索引。如果我们想要切片中除了最后一个元素的所有值,可以这样写:
1 | scores := []int{1, 2, 3, 4, 5} |
上面是从未排序的切片中移除元素的有效方法的开始:
1 | func main() { |
最后,我们已经了解了切片,我们再看另一个通用的内建函数:copy
。正常情况下,将值从一个数组复制到另一个数组的方法有5个参数,source
, sourceStart
,count
,,destination
和 destinationStart
。使用切片,我们仅仅需要两个:
1 | import ( |
花点时间试试上面的代码,并尝试改动。去看看如果你这么做 copy(worst[2:4], scores[:5])
或者复制多于或少于 5
个值给 worst
会发什么?
Go语言中的映射,就好比其他语言中的hash表或者字典。它们的工作方式就是:定义键和值,并且可以获取,设置和删除其中的值。
映射和切片一样,使用 make
方法来创建。让我们来看看一个例子:
1 | func main() { |
我们使用 len
方法类获取映射的键的数量。使用delete
方法来删除一个键对应的值:
1 | // returns 1 |
映射是动态变化的。然而我们可以通过传递第二个参数到 make
方法来设置一个初始大小:
1 | lookup := make(map[string]int, 100) |
如果你事先知道映射会有多少键值,定义一个初始大小将会帮助改善性能。
当你需要将映射作为结构体字段的时候,你可以这样定义它:
1 | type Saiyan struct { |
初始上述结构体的一种方式是:
1 | goku := &Saiyan{ |
Go 还有一种定义和初始化值的方式。像 make
,这种特定用于映射和数组。我们可以定义为复合方式:
1 | lookup := map[string]int{ |
我们可以使用 for
组合 range
关键字迭代映射:
1 | for key, value := range lookup { |
迭代映射是没有顺序的。每次迭代查找将会随机返回键值对。
第二章我们讨论了到底是传值还是传指针。现在我们有相同的问题在映射和数组上,到底该使用他们哪个?
1 | a := make([]Saiyan, 10) |
许多开发者认为应该传递 b
或者返回它在一个函数中会更加高效。然而,传递/返回的是切片的副本,但是切片本身就是一个引用。所以传递返回切片本身,没有什么区别。
当你改变切片或者映射值的时候,你将看到不同。这一点上,和我们在第二章看到的逻辑相同。所以决定使用指针数组还是值数组归结为你如何使用单个值,而不是你用数组还是映射。
Go 中数组和映射的工作方式类似于其他语言。如果你习惯了使用动态数组,这可能就有点小的调整,但是 append
应该能解决你大多的不适应。如果我们超越数组的表面语法,将会发现切片。切片功能强大,并且他们对代码的清晰度产生了巨大的影响。
还有一些边缘情况没有覆盖到,不过你不太可能遇到他们。即使遇到了,希望我们在这里建立的基础帮助你理解正在发生的事情。
现在来看一下如何组织我们的代码。
为了组织复杂的库和系统代码,我们需要学习关于包的知识。在 Go 语言中,包名遵循 Go 项目的目录结构。如果我们建立一个购物系统,我们可能以 “shopping” 包名作为一个开始,然后把所有源代码文件放到 $GOPATH/src/shopping/
目录中。
我们不会去想把所有东西都放在这个文件夹中。例如,我们可能想单独把数据库逻辑放在它自己的目录中。为了实现这个,我们创建一个子目录 $GOPATH/src/shopping/db
。子目录中文件的包名就是 db
,但是为了从另一个包访问它,包括 shopping
包,我们需要导入 shopping/db
。
换句话说,当你想去命名一个包的时候,可以通过 package
关键字,提供一个值,而不是完整的层次结构(例如:「shopping」或者 「db」)。当你想去导入一个包的时候,你需要指定完整路径。
接下来,我们去尝试下。在你的 Go 的工作目录 src
文件夹下(我们已经在基础那一章节中介绍了),创建一个新的文件夹叫做 shopping
,然后在 shopping
文件夹下创建一个 db
文件夹。
在 shopping/db
文件夹下,创建一个叫做 db.go
的文件,然后在 db.go
文件中添加如下的代码:
1 | package db |
需要注意包名和文件夹名是一样的。而且很明显我们实际并没有连接数据库。这里使用这个例子只是为了展示如何组织代码。
现在,创建在主目录 shopping
下创建一个叫 pricecheck.go
的文件。它的内容是:
1 | package shopping |
很有可能认为导入 shopping/db
有点特别,因为我们已经在 shopping
包/目录中。实际上,我们正在导入 $GOPATH/src/shopping/db
,这意味着只要你在你的工作区间 src/test
目录中有一个名为 db
的包,你就可以轻松导入它。
你正在构建一个包,除了我们看到的你不再需要任何东西。为了构建一个可执行程序,你仍然需要 main
包。我比较喜欢的方式是在 shopping
目录下创建一个 main
子目录,然后再创建一个叫 main.go
的文件,下面是它的内容:
1 | package main |
现在,你可以进入你的 shopping
项目运行代码,输入:
1 | go run main/main.go |
当你编写更复杂的系统的时,你必然会遇到循环导入。例如,当 A
包导入 B
包,B
包又导入 A
包(间接或者直接导入)。这是编译器不能允许的。
让我们改变我们的 shopping
结构以复现这个错误。
将 Item
定义从 shopping/db/db.go
移到 shopping/pricecheck.go
。你的 pricecheck.go
文件像下面这样:
1 | package shopping |
如果你尝试运行代码,你会从 db/db.go
得到两个关于 Item
未定义的错误。这看起来是说 Item
不存在 db
包中。它已经被移动到 shopping
包中,我们需要将 shopping/db/db.go
改变成:
1 | package db |
现在但你尝试运行代码的时候,你将会得到 不允许循环导入 的错误。我们可以通过引入另一个包含共享结构体的包来解决这个问题。你的目录现在看起来像这个样子:
1 | $GOPATH/src |
pricecheck.go
将仍然导入 shopping/db
,但是 db.go
现在导入 shopping/models
而不是 shopping
,因此打破了循环。因为我们将共享的 Item
结构体移动到 shopping/models/item.go
,我们现在需要去改变 shopping/db/db.go
从 models
包中引用 Item
结构体。
1 | package db |
你经常需要共享某些代码,不止 models
,所以你可能有其他类似叫做 utilities
的目录,这些共享包的重要原则是他们不从 shopping
包或者任何子包中导入任何东西。在后面的章节中,我们将介绍可以帮助我们解决这些类型依赖关系的接口。
Go 用了一个简单的规则去定义什么类型和函数可以包外可见。如果类型或者函数名称以一个大写字母开始,它就具有了包外可见性。如果以一个小写字母开始,它就不可以。
这也可以应用到结构体字段。如果一个字段名以一个小写字母开始,只有包内的代码可以访问他们。
例如,我们的 items.go
文件中有个这样的函数:
1 | func NewItem() *Item { |
它可以通过 models.NewItem()
这样被调用。但是如果函数命名为 newItem
,我们将不能从不同的包访问它了。
去试试更改 shopping
代码中的函数,类型以及字段的名称。例如,如果你将 Item
的 Price
字段命名为 price
,你应该会获得一个错误。
我们用来 build
和 run
的 go
命令有一个 get
子命令,用于获取第三方库。go get
支持除了这个例子中的各种协议,我们可以从 Github 中获取一个库,意味着,你需要在你的电脑中安装 git
。
假设你已经安装了 Git,在 shell 中输入命令:
1 | go get github.com/mattn/go-sqlite3 |
go get
获取远端的文件并把它们存储在你的工作区间中。去看看你的 $GOPATH/src
目录,你会发现除了我们创建的 shopping
项目之外,还有一个 github.com
目录,在里面,你会看到一个包含了 go-sqlite3
目录的 mattn
目录。
我们刚才只是讨论了如何导入我们工作区间的包。为了导入新安装的 go-sqlite3
包,我们要这样导入:
1 | import ( |
我知道这看起来像一个 URL,实际上,它只是希望导入在 $GOPATH/src/github.com/mattn/go-sqlite3
找到的 go-sqlite3
包。
go get
还有一些其他的技巧。如果我们在一个项目内使用 go get
,它将浏览所有文件,查找 imports
的第三方库然后下载他们。某种程度上,我们的源代码变成了 Gemfile
或者 package.json
。
如果你调用 go get -u
,它将更新所有包(或者你可以通过 go get -u FULL_PACKAGE_NAME
更新一个具体的包)。
最后,你可能发现了 go get
的不足。一方面,这儿没有办法指定一个版本。他总是指向 master/head/trunk/default
。这是一个较大的问题如果你有两个项目需要同一个库的不同版本。
为了解决这个问题,你可以使用一个第三方的依赖管理工具。他们仍然很年轻,但 goop 和 godep 是可信的。更多完整的列表在 go-wiki。
接口是定义了合约但并没有实现的类型。举个例子:
1 | type Logger interface { |
那这样做有什么作用呢?其实,接口有助于将代码与特定的实现进行分离。例如,我们可能有各种类型的日志记录器:
1 | type SqlLogger struct { ... } |
针对接口而不是具体实现的编程会使我们很轻松的修改(或者测试)任何代码都不会产生影响。
你会怎么用?就像任何其它类型一样,它结构可以这样:
1 | type Server struct { |
或者是一个函数参数(或者返回值):
1 | func process(logger Logger) { |
在像 C# 或者 Java 这类语言中,当类实现接口时,我们必须显式的:
1 | public class ConsoleLogger : Logger { |
在 Go 中,下面的情况是隐式发生的。如果你的结构体有一个函数名为 Log
且它有一个 string
类型的参数并没有返回值,那么这个结构体被视为 Logger
。这减少了使用接口的冗长:
1 | type ConsoleLogger struct {} |
Go 倾向于使用小且专注的接口。Go 的标准库基本上由接口组成。像 io
包有一些常用的接口诸如 io.Reader
, io.Writer
, io.Closer
等。如果你编写的函数只需要一个能调用 Close()
的参数,那么你应该接受一个 io.Closer
而不是像 io
这样的父类型。
接口可以成为其他接口的一部分,也就是说接口也可以与其他接口组成新的接口。例如, io.ReadCLoser
的接口是由 io.Reader
接口和 io.Closer
接口组成的。
最后,接口通常用于避免循环导入。由于它们没有具体的实现,因此它们的依赖是有限的。
最后,如何围绕 Go 的工作区间构建你的代码,你只有在写了几个非测试的项目之后才会适应。最重要的是记着包名和目录结构之间的紧密关系(不仅仅在一个项目之内,而是整个工作区间)。
Go 处理类型可见性也是简单有效,而且也是一致的。有一些我们没看过的东西,比如常量和全局变量,但是放心,他们的可见性仍有相同的命名规则决定。
最后,如果你初次接触接口,你可能需要花点时间理解他们。然而,当你第一次看到一个期望类似 io.Reader
的函数时,你会发现自己很感谢作者没有要求他或者她需要的东西。
这章中,我们将讨论 Go 功能杂记,放在其他地方都不太合适。
Go 首选错误处理方式是返回值,而不是异常。考虑 strconv.Atoi
函数,它将接受一个字符串然后将它转换为一个整数。
1 | package main |
你可以创建你自己的错误类型。唯一的要求是你必须实现内建 error
接口的契约:
1 | type error interface { |
更一般地,我们可以通过导入 error
包然后使用它的 New
函数创建我们自己的错误:
1 | import ( |
Go 标准库中有一个使用 error 变量的通用模式。例如, io
包中有一个 EOF
变量它是这样定义的:
1 | var EOF = errors.New("EOF") |
这是一个包级别的变量(被定义在函数之外),可以被其他包访问(首字母大写)。各种函数可以返回这个错误,例如,当我们从一个文件或者 STDIN 读取时。如果它具有上下文意义,那么您应该使用此错误。作为调用者,我们可以这样使用:
1 | package main |
作为最后一点,Go 确实有 panic
和 recover
函数。 panic
就像抛出异常,而 recover
就像 catch
,它们很少使用。
尽管 Go 有一个垃圾回收器,一些资源仍然需要我们显示地释放他们。例如,我们需要在使用完文件之后 Close()
他们。这种代码总是很危险。一方面来说,当我们在写一个函数的时候,很容易忘记关闭我们声明了 10 行的东西。另一方面,一个函数可能有多个返回点。Go 给出的解决方案是使用 defer
关键字:
1 | package main |
如果你尝试运行上面的代码,你将会得到错误(文件不存在)。这里只是演示 defer
如何工作。无论什么情况,在函数返回之后(本例中为 main()
),defer
将被执行。这使您可以在初始化的位置附近释放资源并处理多个返回点。
大多数 Go 程序遵循相同的格式化规则,换句话说,一个 tab 键用于缩进,左括号和他们的声明语句在同一行。
我知道,你可能有自己的风格,并且想坚持它。这也是我长期以来所做的事情,但我很高兴我最终放弃了。一个大原因是 go fmt
命令。它易于使用而且具有权威性(所以就没有人争论无意义的偏好)。
当你在一个项目内的时候,你可以运用格式化规则到这个项目及其所有子目录:
1 | go fmt ./... |
试一试,它不仅缩进你的代码,也对齐了声明的字段和按字母书序导入。
Go 对 if
语句做了稍微修改,支持在条件语句被求值之前先进行初始化:
1 | if x := 10; count > x { |
这是一个比较蠢的例子,更现实的是,你可能会像下面这样做:
1 | if err := process(); err != nil { |
有意思的是,虽然 err
不能在 if
语句之外使用,但他可以在任何 else if
或者 else
之内使用。
在大多数面向对象的语言中,经常有一个内建的叫 object
的基类,是所有其他类的超类。Go 没有继承,也没有这样一个超类。不过他确实有一个没有任何方法的空接口: interface{}
。因为空接口没有方法,可以说所有类型都实现了空接口,并且由于空接口是隐式实现的,因此每种类型都满足空接口契约。
如果我们像,我们可以定义如下签名的 add
函数:
1 | func add(a interface{}, b interface{}) interface{} { |
为了将一个接口变量转化为一个显式的类型,又可以用 .(TYPE)
:
1 | return a.(int) + b.(int) |
提醒,如果底层类型不是 int
,上面的结果将是 error。
你也可以访问强大的类型转换:
1 | switch a.(type) { |
你将会看到,使用空接口可能超出了你的期望。但是虽然它将让代码看起来不那么好看,来回转换代码有时看起来也很丑陋并且危险,但在一个静态语言中,它是唯一的选择。
字符串和字节数组是紧密相关的。我们可以轻松地在他们之间转换:
1 | stra := "the spice must flow" |
实际上,这种转换方式在各种类型之间是通用的。一些函数显示地需要一个 int32
或者 int64
或者它们的无符号部分。你可能发现你必须这样做:
1 | int64(count) |
然而,当它涉及到字节和字符串时,这可能是你经常做的事情。一定记着当你使用 []byte(X)
或者 string(X)
时,你实际上创建了数据的副本。这是必要的,因为字符串是不可变的。
那些由 Unicode 码点 runes
构成的字符串,如果你获取字符串的长度,你可能不能得到你期望的。下面的结果是3:
fmt.Println(len("椒"))
如果你用 range
迭代一个字符串,你将得到 runes,而不是字节。当然,当你将字符串转换为 []byte
类型时,你将得到正确的数据。
函数是一种类型:
1 | type Add func(a int, b int) int |
它可以用在任何地方 – 作为字段类型,参数或者返回值。
1 | package main |
这样使用函数会帮助我们从具实现中解耦代码,更像在使用接口实现。
我们研究了使用 Go 编程的各个方面,最值得注意的是,我们看到了错误的处理行为以及如何释放连接和打开的文件资源。许多人不喜欢 Go 的错误处理方法。这感觉像是倒退了一步。 有时,我同意。然而,我也发现这会让代码更容易理解。 defer
是一种不寻常但是实用的资源管理方法。事实上,它不仅限于资源管理。您可以将 defer
用于任何目的, 比如函数退出时的日志记录。
当然,我们还没有看到 Go 提供的所有花絮,但在你解决遇到的任何问题时你应该感到足够舒服。
Go 通常被描述为一种并发友好的语言。 原因是它提供了两种强大机制的简单语法: 协程 和 通道
协程 类似于一个线程,但是由 Go 而不是操作系统预定。在 协程 中运行的代码可以与其他代码同时运行。我们来看一个例子:
1 | package main |
这里有一些有趣的事情, 但最重要的是我们如何开始一个 协程 。 我们只需使用 go
关键字,然后使用我们想要执行的函数。如果我们只想运行一部分代码, 如上所述, 我们可以使用匿名函数。需要注意的是,匿名函数不只是可以在 协程 中使用,其他地方也可以。
1 | go func() { |
协程 易于创建且开销很小。最终多个 协程 将会在同一个底层的操作系统线程上运行。这通常也称为 M:N 线程模型,因为我们有 M 个应用线程( 协程 )运行在 N 个操作系统线程上。结果就是,一个 协程 的开销和系统线程比起来相对很低(几KB)。在现代的硬件上,有可能拥有数百万个 协程 。
此外,这里还隐藏了映射和调度的复杂性。我们只需要说 这段代码需要同时并发执行 然后让 Go 自己去实现它。
如果我们回到我们的例子中,你将会注意到我们使用 Sleep
让程序等了几毫秒。这是因为主进程在退出前 协程 才会有机会去执行(主进程在退出前不会等待全部 协程 执行完毕)。要解决这个问题,我们需要协调我们的代码。
创建一个协程是微不足道的, 它们开销很小我们可以启动很多; 但是,需要协调并发代码。为了解决这个问题, Go 提供了 通道
。 在我们学习 通道
之前,我认为了解并发编程的基础知识非常重要。
编写并发代码要求您特别注意在哪里读取和写入一个值。 在某些方面, 例如没有垃圾回收的语言 – 它需要您从一个新的角度去考虑您的数据,始终警惕着可能存在的危险。 例如:
1 | package main |
你觉得将会输出什么呢?
如果你认为输出的是 1, 2, ... 20
这既不对也没错。如果你运行了以上的代码确实可能得到这个输出。可是,这个操作就很让人懵逼的。 啥?因为我们可能有多个 (这个情况下两个) 协程 同时写入一个相同变量 counter
。或者,同样糟糕的是,一个协程要读取 counter
时,另一个协程正在写入。
这个真的很危险吗?当然啦! counter++
看起来可能是一行很简单的代码,但它是实际上被拆分为多个汇编语句 – 确切的性质依赖于你跑程序的平台。如果你运行这个例子,你将经常看到那些数字是以一种乱七八糟的顺序打印的,亦或数字是重复的/丢失的。别着急还会有更糟糕的情况, 比方说系统崩溃或者访问并增加任意区块的数据!
从变量中读取变量是唯一安全的并发处理变量的方式。 你可以有想要多少就多少的读取者, 但是写操作必须要得同步。 有太多的方法可以做到这个了,包括使用一些依赖于特殊的 CPU 指令集的真原子操作。然而, 常用的操作还是使用互斥量(译者注:mutex):
1 | package main |
互斥量序列化会锁住锁下的代码访问。因为默认的的 sync.Mutex
是未锁定状态,这儿我们就得先定义 lock sync.Mutex
。
这操作是不看着超简单? 这个例子是具有欺骗性的。当我们进行并发编程时会产生一系列严重的 Bug。 首先,并不是经常能很明显知道什么代码需要保护。使用这样粗糙的锁操作(覆盖着大量代码的锁操作)确实很诱人,这就违背了我们当初进行并发编程的初心了。 我们肯定是需要个优雅的锁操作; 否则,我们最终会把多条快速通道走成单车道的。
另外一个问题是与死锁有关。 使用单个锁时,这没有问题,但是如果你在代码中使用两个或者更多的锁,很容易出现一种危险的情况,当协程A拥有锁 lockA **,想去访问锁 **lockB **,同时协程B拥有锁 **lockB 并需要访问锁 lockA 。
实际上我们使用一个锁时也有可能发生死锁的问题,就是当我们忘记释放它时。 但是这和多个锁引起的死锁行为相比起来,这并不像多锁死锁那样危险(因为这真的 很难发现),当你试着运行下面的代码时,您可以看见发生了什么:
1 | package main |
到现在为止还有很多并发编程我们没有看到过。 首先,有一个常见的锁叫读写互斥锁。它主要提供了两种锁功能: 一个锁定读取和一个锁定写入。它的区别是允许多个同时读取,同时确保写入是独占的。在 Go 中, sync.RWMutex
就是这种锁。另外 sync.Mutex
结构不但提供了Lock
和 Unlock
方法 ,也提供了RLock
和 RUnlock
方法;其中 R
代表 Read.。虽然读写锁很常用,它也给开发人员带来了额外的负担:我们不但要关注我们正在访问的数据,还要注意如何访问。
此外,部分并发编程不只是通过为数不多的代码按顺序的访问变量; 它也需要协调多个协程。 例如,休眠10毫秒并不是一个特别优雅的解决方案。如果一个协程消耗的时间需要超过10毫秒怎么办?如果协程消耗更少的时间而我们浪费周期怎么办?又或者可以等待协程运行完毕, 我们想另外一个协程 嗨, 我有新的数据需要你处理?
这些事在没有 通道
的情况下都是可以完成的。当然对于更简单的情况,我相信你应该 应该 使用基本的功能比如 sync.Mutex
和 sync.RWMutex
, 但正如我们将会在下一节中看到的那样, 通道
旨在让并发编程更简洁和不容易出错。
并发编程的最大调整源于数据的共享。如果你的协程间不存在数据共享,你完全没必要担心同步问题。但是并非所有系统都是如此简单。现实中,许多系统考虑了相反的目的:跨多个请求共享数据。内存缓存和数据库就是最好的例证。这种情况已经成为一个日趋增长的现实。
通道在共享不相关数据的情况下,让并发编程变得更健壮。通道是协程之间用于传递数据的共享管道。换而言之,一个协程可以通过一个通道向另外一个协程传递数据。因此,在任意时间点,只有一个协程可以访问数据。
一个通道,和其他任何变量一样,都有一个类型。这个类型是在通道中传递的数据的类型。例如,创建一个通道用于传递一个整数,我们要这样做:
1 | c := make(chan int) |
这个通道的类型是 chan int
。因此,要将通道传递给函数,我们的函数签名看起来是这个样子的:
1 | func worker(c chan int) { ... } |
通道只支持两个操作:接收和发送。可以这样往通道发送一个数据:
1 | CHANNEL <- DATA |
这样从通道接收数据:
1 | VAR := <-CHANNEL |
箭头预示着数据流向。当发送的时候,数据流向通道。接收的时候,数据流出通道。
在我们开始第一个例子之前还需要知道的是,接收和发送操作是阻塞的。也就是,当我们从一个通道接收的时候, goroutine 将会直到数据可用才会继续执行。类似地,当我们往通道发送数据的时候,goroutine 会等到数据接收到之后才会继续执行。
考虑这样一个系统,我们希望在各个 goroutine 中处理即将到来的数据。这是一个很平常的需求。如果我们在接收数据的 goroutine 上进行数据密集型处理,那么我们可能导致客户端超时。首先,我们先实现我们的 worker。这可能是一个简单的函数,但是我们让它成为结构的一部分,因此我们之前没有看到这样的 goroutines:
1 | type Worker struct { |
我们的 worker 是简单的。他一直等到数据可用然后处理它。尽职尽责,它一直在一个循环中做这个,永远等待更多的数据去处理。
为了去用这个,第一件事情是启动一些 workers:
1 | c := make(chan int) |
然后,给这些 worker 一些活干:
1 | for { |
这里有一个完整的可运行代码:
1 | package main |
我们不知道哪个 worker 将得到什么数据。但我们能确保的是 Go 保证了发送到通道的数据只会被一个接收器接收。
记着,唯一的共享状态时通道,我们可以安全地同时从它接收和发送数据。通道提供了所有我们需要的同步代码保证,在任何时间只有一个 goroutine 可以访问特定的数据。
上面给出的代码中,如果有超过能处理的数据到来会发什么?你可以通过更改 worker 接收到数据之后的暂停时间来模拟这个。
1 | for { |
我们的主代码中发生的是,接收用户数据的代码(刚刚使用随机数生成器模拟的)是阻塞,因为没有接收器可用。
在某些情况下,你可能需要担保数据被处理掉,这个时候就需要开始阻塞客户端。在某些情况下,你可能会降低这种担保。这有几种常用的策略实现它。第一个就是缓冲数据。如果没有worker可用,我们想去临时存储数据在某些队列中。通道内建这种缓冲容量,当我们使用 make
创建通道的时候,可以设置通道的长度:
1 | c := make(chan int, 100) |
你可以对此更改进行更改,但你会注意到处理仍然不稳定。缓冲通道不会增加容量,他们只提供待处理工作的队列,以及处理突然飙升的任务量的好方法。在我们的示例中,我们不断推送比 worker 可以处理的数据更多的数据。
然而,我们实际上可以通过查看通道的 len
来理解缓冲通道是什么。
1 | for { |
你可以看到通道长度一直增加直到满了,这个时候往我们的通道发送数据将再一次阻塞。
即使有缓冲,在某些时候我们需要开始删除消息。我们不能为了让 worker 轻松而耗尽所有内存。为了实现这个,我们使用 Go 的 select
:
语法上,select
看起来有一点像 switch。使用它,我们提供当通道不能发送数据的时候处理代码。首先,让我们移除通道缓冲来看看 select
如何工作:
1 | c := make(chan int) |
接下来,改变我们的 for
循环:
1 | for { |
我们将每秒推送20条消息,但是我们的 worker 每秒仅仅能处理10条。也就是说,一般的消息,将被丢掉。
这只是我们能使用 select
实现的一个开始。select
的主要目的是管理多个通道,select
将阻塞直到第一个通道可用。如果没有通道可用,如果提供了 default
,那么他就会被执行。如果多个通道都可用了,随机挑选一个。
很难用一个简单的例子来证明这个行为,因为它是一个相当高级的功能。下一节可能有助于证明这个。
我们看过了缓冲消息以及简单地将他们丢弃。另一个通用的选择是去超时。我们将阻塞一段时间,但不会永远。这在 Go 中也是很容易实现的。虽然,语法很难遵循,但是这样一个简洁有用的功能我不能将它排除在外。
为了阻塞最长时间,我们可以用 time.After
函数。我们来一起看看它并试着超越魔法。为了去用这个,我们的发送器将变成:
1 | for { |
time.After
返回了一个通道,所以我们在 select
中使用它。这个通道可以在指定时间之后被写入。就这样,没有其他魔法了。如果你比较好奇,这里有一个 after
的实现,看起来大概就是这个样子咯:
1 | func after(d time.Duration) chan bool { |
回到我们的 select
,还有两个东西可以试试。首先,如果添加回 default
会发生什么?能猜到吗?试试它。如果你不确定,记着如果没有可用的通道,default
将会立即触发。
还有,time.After
是一个 chan time.Time
类型的通道。上面的例子中,我们仅仅是简单地丢弃掉了发送到通道的值。如果你想要,你可以接受它:
1 | case t := <-time.After(time.Millisecond * 100): |
注意力重新回到我们的 select
,可以看到我们发送给 c
但是却从 time.After
接收。无论我们从哪里接收,发送给谁,或者任何通道的组合,select
工作方式是相同的:
最后,在 for
中看到一个 select
是很常见的:
1 | for { |
如果你是并发编程的新手,那么看起来似乎都是压倒性的。 它绝对需要非常多的关注。Go旨在让它变得更容易。
Goroutines 有效的抽象了我们需要并发执行的代码。通道帮助消除数据共享时共享数据可能发生的一些严重错误。这不仅可以消除错误, 还可以改变并发编程的方式。你只用考虑通过信息传递实现并发编程,而不是危险的代码区域。
话虽如此,我仍然广泛使用 sync
和 sync / atomic
包中的各种同步原语。我觉得比较重要的是通过使用这两种方式比较舒适。我建议你首先关注通道,但是当你遇到一个需要短暂锁的简单示例时,请考虑使用互斥锁或读写互斥锁。
我最近听说 Go 被描述为一个枯燥的语言。枯燥是因为很容易去学,很容易写,以及最重要的,易读。或许,我确实认为这个实现不太好,毕竟,我确实花了三章讨论类型和如何声明变量。
如果你有静态类型语言的工作经历,我们所看到的内容仅仅只是一个复习。Go 使得指针可用性增强,并且切片是数组的包装,对于经验丰富的 Java 或 C#开发人员来说可能并不算是压倒性优势。
如果你曾经大多在使用动态语言,你可能会感到有点不同。它是一个值得学习的东西。其中最重要的是声明和各种初始化的语法。尽管我是 Go 的粉丝,Go 尽管也在简单性方面取得了一些进展,但它并不简单。不过,它归纳为一些基本的规则(比如你只能声明变量一次以及 :=
确实声明了变量)以及基本理解(比如 new(X)
或者 &X{}
仅仅只是分配内存,但是切片,映射以及通道需要更多的初始化,所以用 make
)。
除了这些,Go 给了我们简单但有效的方式组织我们的代码。接口,基于返回的错误处理,用于资源管理的 defer
以及实现组合的简单方式。
最后但也最重要的是内置并发支持。关于 协程 ,除了有效和简单(无论如何简单易用)之外,几乎没有什么可说的了。这是一个很好的抽象。 通道 更为复杂。我一直认为在使用高级包装器之前先理解最基本使用方法。我认为不通过 通道 学习并发编程是很有用的。但是,对我来说,我觉得 通道 的实现方式不像一个简单的抽象。它们几乎都是自己的基本构件。我这样说是因为它们改变了你编写和思考并发编程的方式。 鉴于并发编程有多么困难,这绝对是一件好事。
以及实现组合的简单方式。
最后但也最重要的是内置并发支持。关于 协程 ,除了有效和简单(无论如何简单易用)之外,几乎没有什么可说的了。这是一个很好的抽象。 通道 更为复杂。我一直认为在使用高级包装器之前先理解最基本使用方法。我认为不通过 通道 学习并发编程是很有用的。但是,对我来说,我觉得 通道 的实现方式不像一个简单的抽象。它们几乎都是自己的基本构件。我这样说是因为它们改变了你编写和思考并发编程的方式。 鉴于并发编程有多么困难,这绝对是一件好事。
说明:例如将代码提交到git仓库,将一些敏感信息提交,所以需要删除提交记录以彻底清除提交信息,以得到一个干净的仓库且代码不变
1 | git checkout –-orphan latest_branch |
1 | git add -A |
1 | git commit -am "commit message" |
1 | git branch -D master |
1 | git branch -m master |
1 | git push -f origin master |
因为权限问题才无法登陆,这里记录下,方便后面查用。
1 | sudo chmod 644 ~/.ssh/authorized_keys |
pypi 镜像每 5 分钟同步一次。
1 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package |
注意,simple 不能少, 是 https 而不是 http
升级 pip 到最新的版本 (>=10.0.0) 后进行配置:
1 | pip install pip -U |
如果您到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:
1 | pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U |
1 | private static final double EARTH_RADIUS = 6372.796924; |
这种写法非常朴实,程序流程也非常明确,但是事务处理与程序流程嵌入太深,容易遗漏,造成严重的问题
1 | func DoSomething() (err error) { |
下面这种写法把事务处理从程序流程抽离了出来,不容易遗漏,但是作用域是整个函数,程序流程不是很清晰
1 | func DoSomething() (err error) { |
写法三是对写法二的进一步封装,写法高级一点,缺点同上
1 | func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) { |
经过总结和实验,我采用了下面这种写法,defer tx.Rollback() 使得事务回滚始终得到执行。 当 tx.Commit() 执行后,tx.Rollback() 起到关闭事务的作用, 当程序因为某个错误中止,tx.Rollback() 起到回滚事务,同事关闭事务的作用。
1 | func DoSomething() (err error) { |
(1) 小事务 每次循环提交一次 在循环内部使用这种写法的时候,defer 不能使用,所以要把事务部分抽离到独立的函数当中
1 | func DoSomething() (err error) { |
(2) 大事务 批量提交 大事务的场景和普通场景是一样的,没有任何区别
1 | func DoSomething() (err error) { |
参考链接:
https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback
原文地址:
http://hopehook.com/2017/08/21/golang_transaction/
从Go1.11开始,golang官方支持了新的依赖管理工具go mod
。
1 | ➜ ~ go mod |
使用go mod并不要求你的项目源码放到$GOPATH下,所以你的新项目可以放到任意你喜欢的路径。在项目根目录下执行go mod init
,会生成一个go.mod文件。然后你可以在其中增加你的依赖,如下:
1 | module github.com/gaoyoubo/xxx |
然后执行go mod download
,将依赖下载到本地。这些依赖并不是下载到你的项目目录下,而是会下载到$GOPATH/pkg/mod
目录下,这样所有使用go mod的项目都可以共用。
在旧项目中使用非常简单,只需要一下两个步骤:
go mod init
: 在项目根目录下执行该命令,会在项目根目录下生成一个go.mod
文件。go mod tidy
: 在项目根目录下执行该命令,go mod会自动分析你当前项目所需要的依赖,并且将他们下载下来。运行 go get -u
将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号y是次要版本号)
运行 go get -u=patch
将会升级到最新的修订版本
运行 go get package@version
将会升级到指定的版本
This one is for all MySQL-DBA’s, which are working on macOS. Since the Apple OS has a rather peculiar way of starting and stopping MySQL, compared to Linux, you can run into some issues. These problems occur especially, if you have no access to the GUI.
Put skip-grant-tables into the mysqld section of the my.cnf. A my.cnf can be found in /usr/local/mysql/support-files. You MUST work as root for all the following steps.
1 | sudo -s |
Save the configuration file! (In vi this is “[ESC] + :x”)
Continue with stopping MySQL:
1 | launchctl unload /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist |
Restart MySQL, so skip-grant-tables becomes active:
1 | launchctl load /Library/LaunchDaemons/com.oracle.oss.mysql.mysqld.plist |
After MySQL is started again, you can log into the CLI and reset the password:
1 | mysql -u root |
If you are not capable of stopping MySQL in a civilised manner, you can use the more rough way. You can send a SIGTERM to the MySQL-Server:
1 | ps -aef | grep mysql | grep -v grep |
You should receive one line. The second column from the left is the process id. Use this process id to stop the MySQL-Server.
1 | kill -15 [process id] |
In this example, the command would look like this:
1 | kill -15 28017 |
macOS will restart MySQL, since the process has not stopped correctly. The configuration will be read and the changes to the parameters will become effective. Continue with logging in to the CLI.
No matter how secure your MySQL-Password is, it is a lot more important to secure access to the server it self. If your server is not secured by something that prevents access from the internet, it will only take a few minutes for someone with bad intentions to take over your database or worse, the entire server.
Github:https://github.com/blindpirate/report-of-build-tools-for-java-and-golang
In January 2017, the usage of build tools in Github’s top 1000 Java repositories is as follows:
Tool Name | Reference Count |
---|---|
Gradle | 627 |
Maven | 264 |
Ant | 52 |
Npm | 4 |
Bazel | 3 |
Make | 1 |
And the trending over the past 8 years is:
Tool Name | Identity Files |
---|---|
Gradle | build.gradle |
Maven | pom.xml |
Ant | build.xml |
Npm | package.json |
Bazel | BUILD |
Make | Makefile/makefile |
groovy GithubTopRankCrawler.groovy -l java -d <path to store the 1000 repos>
to clone all repositories locally. groovy JavaBuildToolScanner.groovy -d <path to store the 1000 repos>
to analyze these repos.There are various package management tools for golang as listed here. But which one is the most popular?
The usage of package manage tools in Github’s top 1000 Go repositories is as follows:
Tool Name | Url | Reference Count (Feb 2017) | Reference Count (Nov 2017) |
---|---|---|---|
Makefile | Makefile | 199 | 181 |
dep | dep | N/A | 94 |
godep | godep | 119 | 90 |
govendor | govendor | 65 | 84 |
glide | glide | 64 | 77 |
gvt | gvt | 25 | 16 |
trash | trash | 7 | 13 |
submodule | submodule | 8 | 6 |
gpm/johnny-deps | gpm johnny-deps | 7 | 6 |
glock | glock | 5 | 4 |
gom | gom | 4 | 2 |
gopack | gopack | 3 | 2 |
gopm | gopm | 3 | 1 |
goop | goop | 1 | 1 |
gvend | gvend | 2 | 0 |
dep had a first release in May 2017, did not exist for first stats.
Technically, make
is not a package management tool, here it is just for comparison.
Submodule refers to a set of tools which use git submodule to manage dependencies such as manul and Vendetta and so on.
Tool Name | Identity Files |
---|---|
godep | Godeps/Godeps.json |
govendor | vendor/vendor.json |
gopm | .gopmfile |
gvt | vendor/manifest |
gvend | vendor.yml |
glide | glide.yaml or glide.lock |
trash | vendor.conf |
gom | Gomfile |
bunch | bunchfile |
goop | Goopfile |
goat | .go.yaml |
glock | GLOCKFILE |
gobs | goproject.json |
gopack | gopack.config |
nut | Nut.toml |
gpm/johnny-deps | Godeps |
Makefile | makefile or Makefile |
submodule | .gitmodules |
groovy GithubTopRankCrawler.groovy -l go -d <path to store the 1000 repos>
to clone all repositories locally. You can use -s
to do the shallow clone and decrease disk usage.groovy GoBuildToolScanner.groovy <path to store the 1000 repos>
to analyze these repos.https://github.com/gaoyoubo/hexo-client/releases/tag/v1.2.4
1 | git clone git@github.com:gaoyoubo/hexo-client.git |
1 | cd existing_folder |
1 | cd existing_repo |
https://github.com/gaoyoubo/hexo-client/releases/tag/v1.2.3
JWT全称为:JSON Web Token是目前最流行的跨域认证的解决方案。
互联网服务离不开用户认证。一般流程是下面这样。
这种模式的问题在于,扩展性不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。
1 | { |
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
实际的 JWT 大概就像下面这样。
它是一个很长的字符串,中间用点.
分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
1 | Header.Payload.Signature |
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
1 | { |
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT令牌统一写为JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
JWT里验证和签名使用的算法,可选择下面的:
JWS | 算法名称 | 描述 |
---|---|---|
HS256 | HMAC256 | HMAC with SHA-256 |
HS384 | HMAC384 | HMAC with SHA-384 |
HS512 | HMAC512 | HMAC with SHA-512 |
RS256 | RSA256 | RSASSA-PKCS1-v1_5 with SHA-256 |
RS384 | RSA384 | RSASSA-PKCS1-v1_5 with SHA-384 |
RS512 | RSA512 | RSASSA-PKCS1-v1_5 with SHA-512 |
ES256 | ECDSA256 | ECDSA with curve P-256 and SHA-256 |
ES384 | ECDSA384 | ECDSA with curve P-384 and SHA-384 |
ES512 | ECDSA512 | ECDSA with curve P-521 and SHA-512 |
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
1 | { |
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
1 | HMACSHA256( |
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用.
分隔,就可以返回给用户。
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+
、/
和=
,在 URL 里面有特殊含义,所以要被替换掉:=
被省略,+
替换成-
,/
替换成_
。这就是Base64URL 算法。
客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization
字段里面。
1 | Authorization: Bearer <token> |
另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。
在升级GO版本到1.11后发现Goland的Debug报错,错误信息如下:
1 | could not launch process: decoding dwarf section info at offset 0x0: too short |
原因是Goland的dlv不是新版本,导致不能debug调试。
解决办法:
HexoClient是一款跨平台的Hexo管理工具。
项目地址:https://github.com/gaoyoubo/hexo-client
欢迎加入HexoClient用户群交流。
我是从2011年开始写博客,在早期的时候wordpress
、zblog
、emlog
等开源的博客程序都是用过。但是本着生命在于则疼的原则,后来我自己使用Java写了个简单的Blog程序( https://gitee.com/gaoyoubo/mlog ) 将其托管在阿里云服务器上。但是后面觉得为了一个博客单独买一台服务器成本比较高,所以后来改用Hexo
+Github Pages
,这样每年基本只需要几十块钱的域名费用即可。开始使用Hexo的时候也只是按照常规方式使用,后来了解到了electron
框架,所以决定利用electron
来为hexo写一个客户端。开始完全是为了自用,开源出去之后反响还不错,收到很多hexo博客党的反馈。
本文不会讲解如何安装、配置、使用Hexo,所以阅读前请确保掌握以下技能。
首先安装hexo
1 | npm install hexo-cli -g |
去Hexo的产品发布页( https://github.com/gaoyoubo/hexo-client/releases )下载你对应平台的安装包进行安装。
成功安装后打开程序会要求弹窗要求填写Hexo项目路径,该路径就是第一步通过hexo init blog
创建的博客路径。正确配置路径之后即可愉快的使用HexoClient。
我在Github上创建以下两个项目:
hexo deploy
之后的静态网页blog-source
有新的提交记录,那么会自动执行脚本将更新发布到blog.mspring.org
。进入Github个人主页,找到:Settings -> Developer settings -> Personal access tokens,然后取Generate new token
,参照下图配置即可。
这里生成的Token,接下来会用到,请先妥善保存好。
使用 GitHub账户登录
Travis-CI官网 ,进去后能看到已经自动关联了 GitHub 上的仓库。这里我们选择需要启用的项目,即 blog-source
。然后点击后面的Settings
进入设置界面。
进入设置界面后可以参考我的配置:
配置主要注意一下两点即可:
当分支收到新的push之后构建
GH_TOKEN,是我们第一步在github中生成的access token,因为要从github上将代码拉到travis-ci机器上进行构建,所以需要该token授权。
因为我们的博客托管在github pages,所以我们是以git的方式进行deploy的,hexo如何配置使用git方式进行deploy,请自行Google。下面截取了我的_config.yml
文件中关于git deploy配置的片段。
1 | Deployment |
.travis.yml
在hexo项目的根目录创建.travis.yml
文件,该文件就是travis的构建脚本,下面是我的脚本配置,我会在脚本中详细注释每一步的作用。
1 | 指定语言为node_js,nodejs版本stable |
在菜单栏中找到:查看 -> 切换开发者工具,将开发者工具打开,然后看控制台是否有错误,如果有错误将错误信息copy出来,点击这里提交问题:https://github.com/gaoyoubo/hexo-client/issues/new
HexoClient的数据加载是完全依赖于Hexo的,所以在打开HexoClient之前要确保你的Hexo是install成功的。
ctrl+s
多次保存会生成多篇文章的问题front-matter
#32 #38electron
到最新版本webpack
到最新版本,解决老版本漏洞问题1 | { |
electron-builder支持构建多个平台安装包,上面的配置中我配置了Windows、macos、linux,可以直接拷贝使用,如果想了解更多可以看这篇官方出品的文档:https://www.electron.build/configuration/configuration
1 | node .electron-vue/build.js && electron-builder --publish onTagOrDraft |
可以看到后面的参数--publish onTagOrDraft
他的意思是,当在标签中提交,或github中存在draft发布版本的时候触发publish操作,这个时候会自动将构建好的包上传到github releases中。publish配置的取值如下:
Value | Description |
---|---|
onTag | on tag push only |
onTagOrDraft | on tag push or if draft release exists |
always | always publish |
never | never publish |
https://github.com/gaoyoubo/hexo-client/releases
之前版本都只编译了Macos版本安装包,这次特意安装了一个虚拟机将Windows版本也编译了一份。
周末没事将自己闲置的Thinkpad安装了最新的Ubuntu18.10版本,安装成功之后就想着将之前在自己的vps上配置的shadowsock服务使用上。
1 | sudo apt-get install shadowsocks |
安装完成之后默认的配置文件在/etc/shadowsocks/local.json
,去将里面的配置修改成自己的即可。
1 | { |
1 | sudo sslocal -c /etc/shadowsocks/local.json -d start |
1 | sudo pip install genpac |
用来存放用户自定义规则列表文件user-rules.txt和生成后的autoproxy.pac文件,例如我的放在home目录下
1 | mkdir ~/soft/pac |
我使用的是github上托管的这份文件:https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt
执行一下命令来创建autoprox.pac
1 | genpac --pac-proxy "SOCKS5 127.0.0.1:1080" --output="autoproxy.pac" --gfwlist-url="https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt" --user-rule-from="user-rules.txt" |
去Ubuntu设置 -> 网络 -> 代理设置
设置代理,选择自动
,配置url填写你本地的文件路径,例如:file:///home/xxx/soft/pac/autoproxy.pac
Ubuntu新版本中已经不使用rc.local这种自启动方式了,熟悉旧版本的同学肯定是很不习惯的。那么新版本中如何配置rc.local呢。
自行创建 /etc/rc.local 添加以下默认内容
1 | !/bin/sh -e |
在 exit 0 之前加入自定义内容
执行以下命令确保 rc.local 开机自启
1 | sudo chown root:root /etc/rc.local |
Hexo1.2.0发布,全新的UI布局。
之前一直觉得HexoClient的ui太不像一个原生应用,一眼就能看出是网页做的。同样是基于electron,vscode、atom等应用的ui看起来就特别正规、好看,所以这次周末抽一天时间调整一下UI。
Mac版:https://pan.baidu.com/s/1E-5KrusoBuFGRTunTBqPJQ 提取码:2v2q
Windows版本:请自行源码编译。
Linux版本:请自行源码编译。
Github: https://github.com/gaoyoubo/hexo-client
码云:https://gitee.com/gaoyoubo/hexo-client
之前一直使用朋友的vpn进行翻墙,今天突然发现用不了了,同事强烈推荐使用搬瓦工
( https://bwh8.net ),他们反馈很稳定。于是就去官网上买了一个最低配的,配置如下:
1 | SSD: 10 GB RAID-10 |
价格:$19.99/year
购买的时候可以输入优惠码,优惠码自行百度去,有很多资源。我找了个优惠码,优惠了6.25%
接下来就是配置Shadowsocks Server了, 之前网上的教程中都会看到虚拟机vps的控制台会有一个Shadowsocks Server的选项,进去之后能够一键安装配置该服务,但是我的控制台却没有这个选项,在网上找到了下面一段话。
注:最近很多新购买的服务器在VPS管理面板没有“Shadowsocks server”这一项,若是发生此原因,请按如下操作即可正常安装!若页面中没有Shadowsocks Server这一项,说明一键搭建SS功能被去掉了,这时候需要在当前浏览器的新标签中打开以下网址:
https://kiwivm.64clouds.com/main-exec.php?mode=extras_shadowsocks 打开以后就是安装页面,点击页面中的Install Shadowsocks Server安装即可(安装前提是服务器已打开已运行)。
目前已经按照上面链接中的步骤搞定翻墙了。
另外附上Shadowsocks-NG下载地址:https://github.com/shadowsocks/ShadowsocksX-NG/releases
前几天突然有个姑娘加我的QQ(不知道哪儿来的我的QQ),让我参加他们免费的公开课,然后给我分享Java学习资料,我以为是会给我发几本书,就参加了,没想到是一个txt文件😂,内容如下:
Allen-架构师必备技能-分库分表应对数据量过大
链接:https://pan.baidu.com/s/1OF4RUHvRk98pBRdUiifH2g 密码:n4ev
Allen-互联网安全话题-使用https保障你的敏感数据不再裸奔
链接:https://pan.baidu.com/s/1qz23y-3ahaGua4YH02KTyw 密码:fgh0
Tony-多线程Future模式-写出支撑海量并发连接的服务端代码
链接:https://pan.baidu.com/s/1NwzNRxUB0_DPNQo2IW_Xhg 密码:0fpw
Tony-前后端分离架构分析与实现
链接:https://pan.baidu.com/s/1b7XnTibtqW26YCHfuAXkyA 密码:ah24
Tony-高并发系统架构之负载均衡全方位解析
链接:https://pan.baidu.com/s/1a87EH1Xe20O4XYZaNRo-hw 密码:p52e
Tony-学会举一反三-从Redis看RPC原理
链接:https://pan.baidu.com/s/1disSAbJo-01ESCu6_rTHYQ 密码:ih47
-Mike-分布式系统架构技能—zookeeper实现分布式锁
链接:https://pan.baidu.com/s/1adhFuoUsz1sMQTnWNGoKPA 密码:gjzh
Tony-数据库连接池原理源码分析
链接:https://pan.baidu.com/s/1uBiBBt-tJVSz_5t5p4jG3A 密码:jqo6
Allen-深入SpringMVC原理老司机带你手写自己的MVC框架
链接:https://pan.baidu.com/s/1rlhZCSqXaZpXWM_V5CA7EQ 密码:vysj
Tony-JVM类加载机制之JAVA热部署实战开发
链接:https://pan.baidu.com/s/1JSLGrG0k44um7weQcq5rvg 密码:twn3
Tony-实战高并发系统缓存雪崩场景重现及解决方案
链接:https://pan.baidu.com/s/1i8Q7sPNEcUIPYBuqFerRwQ 密码:lwgj
Mike-解密spring-boot-starter
链接:https://pan.baidu.com/s/12-1N3RTb68l3QfUOxSG1jQ 密码:sodb
Tony-细说springcloud微服务架构之客户端负载均衡
链接:https://pan.baidu.com/s/1VK3mMTkKYzRU4G9YXwlOLg 密码:achq
这台服务由三台机器负载,每日处理一亿四千万+次请求,线上稳定运营一年多,未出现任何故障,如果不是这次需求变动还能继续稳定运行。在重启县截图纪念一下。
转载自:http://ibillxia.github.io/blog/2014/03/10/top-10-open-source-recommendation-systems/
最近这两年推荐系统特别火,本文搜集整理了一些比较好的开源推荐系统,即有轻量级的适用于做研究的SVDFeature、LibMF、LibFM等,也有重量级的适用于工业系统的 Mahout、Oryx、EasyRecd等
主页:http://svdfeature.apexlab.org/wiki/Main_Page 语言:C++
一个feature-based协同过滤和排序工具,由上海交大Apex实验室开发,代码质量较高。在KDD Cup 2012中获得第一名,KDD Cup 2011中获得第三名,相关论文 发表在2012的JMLR中,这足以说明它的高大上。
SVDFeature包含一个很灵活的Matrix Factorization推荐框架,能方便的实现SVD、SVD++等方法, 是单模型推荐算法中精度最高的一种。SVDFeature代码精炼,可以用 相对较少的内存实现较大规模的单机版矩阵分解运算。另外含有Logistic regression的model,可以很方便的用来进行ensemble。
主页:http://www.csie.ntu.edu.tw/~cjlin/libmf/ 语言:C++
作者Chih-Jen Lin来自大名鼎鼎的台湾国立大学,他们在机器学习领域享有盛名,近年连续多届KDD Cup竞赛上均 获得优异成绩,并曾连续多年获得冠军。台湾大学的风格非常务实,业界常用的LibSVM, Liblinear等都是他们开发的,开源代码的效率和质量都非常高。
LibMF在矩阵分解的并行化方面作出了很好的贡献,针对SGD(随即梯度下降)优化方法在并行计算中存在的locking problem和memory discontinuity问题,提出了一种 矩阵分解的高效算法FPSGD(Fast Parallel SGD),根据计算节点的个数来划分评分矩阵block,并分配计算节点。系统介绍可以见这篇 论文(ACM Recsys 2013的 Best paper Award)。
主页:http://www.libfm.org/ 语言:C++
作者是德国Konstanz大学的Steffen Rendle,他用LibFM同时玩转KDD Cup 2012 Track1和Track2两个子竞赛单元,都取得了很好的成绩,说明LibFM是非常管用的利器。
LibFM是专门用于矩阵分解的利器,尤其是其中实现了MCMC(Markov Chain Monte Carlo)优化算法,比常见的SGD优化方法精度要高,但运算速度要慢一些。当然LibFM中还 实现了SGD、SGDA(Adaptive SGD)、ALS(Alternating Least Squares)等算法。
主页:http://lenskit.grouplens.org/ 语言Java
这个Java开发的开源推荐系统,来自美国的明尼苏达大学的GroupLens团队,也是推荐领域知名的测试数据集Movielens的作者。
该源码托管在GitHub上,https://github.com/grouplens/lenskit。主要包含lenskit-api,lenskit-core, lenskit-knn,lenskit-svd,lenskit-slopone,lenskit-parent,lenskit-data-structures,lenskit-eval,lenskit-test等模块,主要实现了k-NN,SVD,Slope-One等 典型的推荐系统算法。
主页:GraphLab - Collaborative Filtering 语言:C++
Graphlab是基于C++开发的一个高性能分布式graph处理挖掘系统,特点是对迭代的并行计算处理能力强(这方面是hadoop的弱项),由于功能独到,GraphLab在业界名声很响。 用GraphLab来进行大数据量的random walk或graph-based的推荐算法非常有效。Graphlab虽然名气比较响亮(CMU开发),但是对一般数据量的应用来说可能还用不上。
GraphLab主要实现了ALS,CCD++,SGD,Bias-SGD,SVD++,Weighted-ALS,Sparse-ALS,Non-negative Matrix Factorization,Restarted Lanczos Algorithm等算法。
主页:http://mahout.apache.org/ 语言:Java
Mahout 是 Apache Software Foundation (ASF) 开发的一个全新的开源项目,其主要目标是创建一些可伸缩的机器学习算法,供开发人员在 Apache 在许可下免费 使用。Mahout项目是由 Apache Lucene社区中对机器学习感兴趣的一些成员发起的,他们希望建立一个可靠、文档翔实、可伸缩的项目,在其中实现一些常见的用于 聚类和分类的机器学习算法。该社区最初基于 Ngetal. 的文章 “Map-Reduce for Machine Learning on Multicore”,但此后在发展中又并入了更多广泛的机器学习 方法,包括Collaborative Filtering(CF),Dimensionality Reduction,Topic Models等。此外,通过使用 Apache Hadoop 库,Mahout 可以有效地扩展到云中。
在Mahout的Recommendation类算法中,主要有User-Based CF,Item-Based CF,ALS,ALS on Implicit Feedback,Weighted MF,SVD++,Parallel SGD等。
主页:http://myrrix.com/ 语言:Java
Myrrix最初是Mahout的作者之一Sean Owen基于Mahout开发的一个试验性质的推荐系统。目前Myrrix已经是一个完整的、实时的、可扩展的集群和推荐系统,主要 架构分为两部分:服务层:在线服务,响应请求、数据读入、提供实时推荐;计算层:用于分布式离线计算,在后台使用分布式机器学习算法为服务层更新机器学习 模型。Myrrix使用这两个层构建了一个完整的推荐系统,服务层是一个HTTP服务器,能够接收更新,并在毫秒级别内计算出更新结果。服务层可以单独使用,无需 计算层,它会在本地运行机器学习算法。计算层也可以单独使用,其本质是一系列的Hadoop jobs。目前Myrrix以被 Cloudera 并入Oryx项目。
主页:http://easyrec.org/ 语言:Java
EasyRec是一个易集成、易扩展、功能强大且具有可视化管理的推荐系统,更像一个完整的推荐产品,包括了数据录入模块、管理模块、推荐挖掘、离线分析等。 EasyRec可以同时给多个不同的网站提供推荐服务,通过tenant来区分不同的网站。架设EasyRec服务器,为网站申请tenant,通过tenant就可以很方便的集成到 网站中。通过各种不同的数据收集(view,buy.rating)API收集到网站的用户行为,EasyRec通过离线分析,就可以产生推荐信息,您的网站就可以通过 Recommendations和Community Rankings来进行推荐业务的实现。
主页:http://waffles.sourceforge.net/ 语言:C++
Waffles英文原意是蜂蜜甜饼,在这里却指代一个非常强大的机器学习的开源工具包。Waffles里包含的算法特别多,涉及机器学习的方方面面,推荐系统位于 其中的Waffles_recommend tool,大概只占整个Waffles的1/10的内容,其它还有分类、聚类、采样、降维、数据可视化、音频处理等许许多多工具包,估计 能与之媲美的也就数Weka了。
主页:http://rapidminer.com/ 语言:Java
RapidMiner(前身是Yale)是一个比较成熟的数据挖掘解决方案,包括常见的机器学习、NLP、推荐、预测等方法(推荐只占其中很小一部分),而且带有GUI的 数据分析环境,数据ETL、预处理、可视化、评估、部署等整套系统都有。另外RapidMiner提供commercial license,提供R语言接口,感觉在向着一个商用的 数据挖掘公司的方向在前进。
开源的推荐系统大大小小的还有很多,以上只是介绍了一些在学术界和工业界比较流行的TOP 10,而且基本上都是用C++/Java实现的,在参考资料[1]、[2]中还提 到的有Crab(Python)、CofiRank(C++)、MyMediaLite(.NET/C#)、PREA(Java)、Python-recsys(Python)、Recommendable(Ruby)、Recommenderlab(R)、 Oryx(Java)、recommendify(Ruby)、RecDB(SQL)等等,当然GitHub上还有更多。。。即有适合单机运行的,也有适合集群的。虽然使用的编程语言不同,但实现 的算法都大同小异,主要是SVD、SGD、ALS、MF、CF及其改进算法等。
转载这篇文章之后找到了官方的文档,建议官方文档,官方文档描述更全面。官方文档地址:https://github.com/tesseract-ocr/tesseract/wiki/Compiling
1 | brew info tesseract |
The result of recognition on Chinese - Simplified
is a little bit terrifying.
I noticed that it added a new neural network system based on LSTMs after 4.0.0+
But it need to be build from source code on macOS.
Thankfully, the manul is quit specify on their README.md
1 | brew install automake autoconf autoconf-archive libtool |
1 | git clone https://github.com/tesseract-ocr/tesseract/ |
Their best trained modes, download the language chi_sim.traineddata and put it under tesseract/4.0.0.1/tessdata/
1 | tesseract image.png image -l chi_sim |
OK, it is still terrible under the Song typeface
font. It need to be trained a new model by myself.
文章转载自:http://artwalk.github.io/2018/05/06/How-to-build-tesseract-4-beta-on-macOS/
JavaCV 是一款开源的视觉处理库,基于GPLv2协议,对各种常用计算机视觉库封装后的一组jar包,封装了OpenCV、ffmpeg、videoInput…等计算机视觉编程人员常用库的接口。
1 | <properties> |
1 | /** |
1 | private static class VideoRecorder implements Closeable { |
这段代码是我四年前写的,当时的使用场景为使用tesseract做图片的预处理。功能包含图片二值化、移除杂色、横向切分、水平切分等。
1 | import java.awt.Color; |