批量拉取代码

场景:目录中有多个git项目,想要更新批量拉取一下这些项目的最新代码。

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/sh
for dir in $(ls -d */); do
cd $dir
if [ -d ".git" ]; then
branch=$(git symbolic-ref --short -q HEAD)
if [ "$branch" != "master" ]; then
echo "skip $dir branch: $branch"
else
echo "dir:$dir branch: $branch"
git pull origin $branch
fi
fi
cd ..
done

扫码目录下的所有项目,如果发现当前项目在master分支,那么pull一下代码。

MacOS安装PHP

偶然的机会需要用一下PHP,记录下安装方式。本身mac系统是自带php的,但是自带的修改起来及其不方便,不好安装扩展。所以直接使用brew安装。

brew安装php

1
2
3
4
brew search php  使用此命令搜索可用的PHP版本
brew install php@7.3.21 使用此命令安装指定版本的php
brew install brew-php-switcher 安装php多版本切换工具
brew-php-switcher 7.3.21 切换PHP版本到7.3.21(需要brew安装多个版本)

安装PHP扩展

1
2
3
4
5
pecl version 查看版本信息
pecl help 可以查看命令帮助
pecl search redis 搜索可以安装的扩展信息
pecl install redis 安装扩展
pecl install http://pecl.php.net/get/redis-4.2.0.tgz 安装指定版本扩展

mac iterm2 rz与sz的功能

本文主要介绍mac环境下使用iterm2rz sz功能的安装流程。

1. 安装lrzsz

1
brew install lrzsz

2. 安装执行脚本

iterm2-send-zmodem.shiterm2-recv-zmodem.sh保存到/usr/local/bin目录下。

iterm2-send-zmodem.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash
# Author: Matt Mastracci (matthew@mastracci.com)
# AppleScript from http://stackoverflow.com/questions/4309087/cancel-button-on-osascript-in-a-bash-script
# licensed under cc-wiki with attribution required
# Remainder of script public domain

osascript -e 'tell application "iTerm2" to version' > /dev/null 2>&1 && NAME=iTerm2 || NAME=iTerm
if [[ $NAME = "iTerm" ]]; then
FILE=$(osascript -e 'tell application "iTerm" to activate' -e 'tell application "iTerm" to set thefile to choose file with prompt "Choose a file to send"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")")
else
FILE=$(osascript -e 'tell application "iTerm2" to activate' -e 'tell application "iTerm2" to set thefile to choose file with prompt "Choose a file to send"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")")
fi
if [[ $FILE = "" ]]; then
echo Cancelled.
# Send ZModem cancel
echo -e \\x18\\x18\\x18\\x18\\x18
sleep 1
echo
echo \# Cancelled transfer
else
/usr/local/bin/sz "$FILE" --escape --binary --bufsize 4096
sleep 1
echo
echo \# Received "$FILE"
fi

iterm2-recv-zmodem.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# Author: Matt Mastracci (matthew@mastracci.com)
# AppleScript from http://stackoverflow.com/questions/4309087/cancel-button-on-osascript-in-a-bash-script
# licensed under cc-wiki with attribution required
# Remainder of script public domain

osascript -e 'tell application "iTerm2" to version' > /dev/null 2>&1 && NAME=iTerm2 || NAME=iTerm
if [[ $NAME = "iTerm" ]]; then
FILE=$(osascript -e 'tell application "iTerm" to activate' -e 'tell application "iTerm" to set thefile to choose folder with prompt "Choose a folder to place received files in"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")")
else
FILE=$(osascript -e 'tell application "iTerm2" to activate' -e 'tell application "iTerm2" to set thefile to choose folder with prompt "Choose a folder to place received files in"' -e "do shell script (\"echo \"&(quoted form of POSIX path of thefile as Unicode text)&\"\")")
fi

if [[ $FILE = "" ]]; then
echo Cancelled.
# Send ZModem cancel
echo -e \\x18\\x18\\x18\\x18\\x18
sleep 1
echo
echo \# Cancelled transfer
else
cd "$FILE"
/usr/local/bin/rz --rename --escape --binary --bufsize 4096
sleep 1
echo
echo
echo \# Sent \-\> $FILE
fi

3. 赋予这两个文件可执行权限

1
chmod 777 /usr/local/bin/iterm2-*

4. 设置Iterm2的Tirgger特性

设置Iterm2的Tirgger特性,profiles->default->editProfiles->Advanced中的Tirgger

添加两条trigger,分别设置 Regular expression,Action,Parameters,Instant如下:

1
2
3
4
5
6
7
8
9
Regular expression: rz waiting to receive.\*\*B0100
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-send-zmodem.sh
Instant: checked

Regular expression: \*\*B00000000000000
Action: Run Silent Coprocess
Parameters: /usr/local/bin/iterm2-recv-zmodem.sh
Instant: checked

示例图:

5. 使用

  • 上传文件:rz
  • 下载文件:sz + file

参考:

在命令行中压缩图片

今天有需求将一些非常大的图片压缩一下,本来想自己写代码进行压缩的,但是觉得这是一个非常常见的需求,应该有现成的解决方案,于是Google了一下,找到了这两个工具:jpegoptim、optipng

安装

我是在MacOS中安装的,Linux上应该也有这个两个工具,请自行摸索

我使用的是brew进行安装,命令如下:

1
2
brew install jpegoptim
brew install optipng

jpegoptim 使用

1
2
3
4
5
6
7
8
# 压缩
jpegoptim file.jpg

# 指定大小压缩
jpegoptim --size=1024k file.jpg

# 移除Exif信息
jpegoptim --strip-exif file.jpg

optipng 使用

1
optipng file.png

AndroidStudio连接mumu模拟器方法

启动mumu模拟器之后在设备列表中找不到模拟器,于是在网上搜索了下教程。

有个教程提供一下方法:

1
adb connect 127.0.0.1:7555 

附模拟器端口:

1
2
3
4
5
6
夜神模拟器:adb connect 127.0.0.1:62001
逍遥安卓模拟器:adb connect 127.0.0.1:21503
天天模拟器:adb connect 127.0.0.1:6555
海马玩模拟器:adb connect 127.0.0.1:53001
网易MUMU模拟器:adb connect 127.0.0.1:7555
原生模拟器:adb connect (你的IP地址):5555

但是这个方法在mac下好像不还用,于是又找到另外一种方法:

1
adb kill-server && adb server && adb shell

这个方法终于生效。

Centos安装mariadb-server

安装

1
yum install mariadb-server

注意:安装之后初始密码为空

常用命令

1
2
3
systemctl start mariadb #启动服务
systemctl enable mariadb #设置开机启动
systemctl restart mariadb #重新启动

初始化

执行命令mysql_secure_installation进行初始化,过程中会让你设置root密码等信息,自己按照提示一步步来即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
[root@iZj6chtv8h63huh6sbynuiZ ~]# mysql_secure_installation

NOTE: RUNNING ALL PARTS OF THIS SCRIPT IS RECOMMENDED FOR ALL MariaDB
SERVERS IN PRODUCTION USE! PLEASE READ EACH STEP CAREFULLY!

In order to log into MariaDB to secure it, we'll need the current
password for the root user. If you've just installed MariaDB, and
you haven't set the root password yet, the password will be blank,
so you should just press enter here.

Enter current password for root (enter for none):
OK, successfully used password, moving on...

Setting the root password ensures that nobody can log into the MariaDB
root user without the proper authorisation.

Set root password? [Y/n] Y
New password:
Re-enter new password:
Password updated successfully!
Reloading privilege tables..
... Success!


By default, a MariaDB installation has an anonymous user, allowing anyone
to log into MariaDB without having to have a user account created for
them. This is intended only for testing, and to make the installation
go a bit smoother. You should remove them before moving into a
production environment.

Remove anonymous users? [Y/n] Y
... Success!

Normally, root should only be allowed to connect from 'localhost'. This
ensures that someone cannot guess at the root password from the network.

Disallow root login remotely? [Y/n] Y
... Success!

By default, MariaDB comes with a database named 'test' that anyone can
access. This is also intended only for testing, and should be removed
before moving into a production environment.

Remove test database and access to it? [Y/n] Y
- Dropping test database...
... Success!
- Removing privileges on test database...
... Success!

Reloading the privilege tables will ensure that all changes made so far
will take effect immediately.

Reload privilege tables now? [Y/n] Y
... Success!

Cleaning up...

All done! If you've completed all of the above steps, your MariaDB
installation should now be secure.

Thanks for using MariaDB!

shadowsocks go 一键安装

本脚本适用环境:

系统支持: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
2
3
wget --no-check-certificate https://raw.githubusercontent.com/iMeiji/shadowsocks_install/master/shadowsocks-go.sh
chmod +x shadowsocks-go.sh
./shadowsocks-go.sh 2>&1 | tee shadowsocks-go.log

安装完成后,脚本提示如下:

1
2
3
4
5
6
7
8
9
Congratulations, shadowsocks-go install completed!
Your Server IP:your_server_ip
Your Server Port:your_server_port
Your Password:your_password
Your Local Port:1080
Your Encryption Method:aes-256-cfb

Welcome to visit:http://teddysun.com/392.html
Enjoy it!

卸载方法:

使用 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
2
3
4
5
6
7
8
9
10
11
{
"port_password":{
"8989":"password0",
"9001":"password1",
"9002":"password2",
"9003":"password3",
"9004":"password4"
},
"method":"aes-256-cfb",
"timeout":600
}

参考链接:
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导出指定列的数据

MySql导出整库或者指定表的数据使用mysqldump命令即可,但是导出表中指定列的数据就需要用到下面命令了,如下:

1
mysql -uroot -p123456 database_name -e "SELECT name from t_xxx where type = 3 INTO OUTFILE'/data/xxx.sql'"

xxx is not in the sudoers file解决方法

用sudo时提示”xxx is not in the sudoers file. This incident will be reported.其中XXX是你的用户名,也就是你的用户名没有权限使用sudo,我们只要修改一下/etc/sudoers文件就行了。下面是修改方法:

  1. 进入超级用户模式。也就是输入”su -“,系统会让你输入超级用户密码,输入密码后就进入了超级用户模式。(当然,你也可以直接用root用)
  2. 添加文件的写权限。也就是输入命令chmod u+w /etc/sudoers
  3. 编辑/etc/sudoers文件。也就是输入命令vim /etc/sudoers,输入”i”进入编辑模式,找到这一 行:root ALL=(ALL) ALL在起下面添加xxx ALL=(ALL) ALL(这里的xxx是你的用户名),然后保存(就是先按一 下Esc键,然后输入”:wq”)退出。
  4. 撤销文件的写权限。也就是输入命令chmod u-w /etc/sudoers

Use Go Channels as Promises and Async/Await

原文地址: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.


Single async/await

First, let’s experiment with a simple use case: await a result from an async function.

1
2
3
4
5
6
7
8
9
10
// Javascript.

const longRunningTask = async () => {
// Simulate a workload.
sleep(3000)
return Math.floor(Math.random() * Math.floor(100))
}

const r = await longRunningTask()
console.log(r)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Go.

package main

import (
"fmt"
"math/rand"
"time"
)

func longRunningTask() <-chan int32 {
r := make(chan int32)

go func() {
defer close(r)

// Simulate a workload.
time.Sleep(time.Second * 3)
r <- rand.Int31n(100)
}()

return r
}

func main() {
r := <-longRunningTask()
fmt.Println(r)
}

Single async/await in Javascript vs. Golang

To declare an “async” function in Go:

  • The return type is <-chan ReturnType.
  • Within the function, create a channel by make(chan ReturnType) and return the created channel at the end of the function.
  • Start an anonymous goroutine by go func() {...} and implement the function’s logic inside that anonymous function.
  • Return the result by sending the value to channel.
  • At the beginning of the anonymous function, add defer close(r) to close the channel once done.

To “await” the result, simply read the value from channel by v := <- fn().


Promise.all()

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
2
3
4
5
6
7
8
9
10
// Javascript.

const longRunningTask = async () => {
// Simulate a workload.
sleep(3000)
return Math.floor(Math.random() * Math.floor(100))
}

const [a, b, c] = await Promise.all(longRunningTask(), longRunningTask(), longRunningTask())
console.log(a, b, c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Go.

package main

import (
"fmt"
"math/rand"
"time"
)

func longRunningTask() <-chan int32 {
r := make(chan int32)

go func() {
defer close(r)

// Simulate a workload.
time.Sleep(time.Second * 3)
r <- rand.Int31n(100)
}()

return r
}

func main() {
aCh, bCh, cCh := longRunningTask(), longRunningTask(), longRunningTask()
a, b, c := <-aCh, <-bCh, <-cCh

fmt.Println(a, b, c)
}

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.


Promise.race()

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Javascript.

const one = async () => {
// Simulate a workload.
sleep(Math.floor(Math.random() * Math.floor(2000)))
return 1
}

const two = async () => {
// Simulate a workload.
sleep(Math.floor(Math.random() * Math.floor(1000)))
sleep(Math.floor(Math.random() * Math.floor(1000)))
return 2
}

const r = await Promise.race(one(), two())
console.log(r)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Go.

package main

import (
"fmt"
"math/rand"
"time"
)

func one() <-chan int32 {
r := make(chan int32)

go func() {
defer close(r)

// Simulate a workload.
time.Sleep(time.Millisecond * time.Duration(rand.Int63n(2000)))
r <- 1
}()

return r
}

func two() <-chan int32 {
r := make(chan int32)

go func() {
defer close(r)

// Simulate a workload.
time.Sleep(time.Millisecond * time.Duration(rand.Int63n(1000)))
time.Sleep(time.Millisecond * time.Duration(rand.Int63n(1000)))
r <- 2
}()

return r
}

func main() {
var r int32
select {
case r = <-one():
case r = <-two():
}

fmt.Println(r)
}

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.


Promise.then() and Promise.catch()

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中统一包装响应

SpringBoot 中可以基于 ControllerAdviceHttpMessageConverter 实现对数据返回的包装。

实现如下,先来写一个 POJO 来定义一下返回格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import com.example.demo.common.exception.base.ErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public class Response<T> {

private int code = HttpStatus.OK.value();

private String msg = "success";

private T data;

public Response(T data) {
this.data = data;
}

public Response(int code, String msg) {
this.code = code;
this.msg = msg;
}

public Response(int code, T data) {
this.code = code;
this.data = data;
}

public Response(ErrorCode errorCode) {
this.code = errorCode.getCode();
this.msg = errorCode.getMessage();
}

public Response(ErrorCode errorCode, T data) {
this.code = errorCode.getCode();
this.msg = errorCode.getMessage();
this.data = data;
}
}

这里用到了lomboklombok的使用介绍不在本文范围内。

用一个 ResponseBodyAdvice 类的实现包装 Controller 的返回值:

以下是我以前的实现方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import com.example.demo.common.RequestContextHolder;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@ControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice {
private static Logger logger = LoggerFactory.getLogger(FormatResponseBodyAdvice.class);

@Autowired
private ObjectMapper objectMapper;

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

Object wrapperBody = body;
try {
if (!(body instanceof Response)) {
if (body instanceof String) {
wrapperBody = objectMapper.writeValueAsString(new Response<>(body));
} else {
wrapperBody = new Response<>(body);
}
}
} catch (Exception e) {
logger.error("request uri path: {}, format response body error", request.getURI().getPath(), e);
}
return wrapperBody;
}

}

为什么要对返回类型是 String 时进行特殊处理呢?因为如果直接返回 new Response<>(body) 的话,在使用时返回 String 类型的话,会报类型转换异常,当时也没有理解什么原因导致的,所以最后使用了 jacksonResponse 又做了一次序列化。

今天找到了导致这个异常的原因:

因为在所有的 HttpMessageConverter 实例集合中,StringHttpMessageConverter 要比其它的 Converter 排得靠前一些。我们需要将处理 Object 类型的 HttpMessageConverter 放得靠前一些,这可以在 Configuration 类中完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfiguration implements WebMvcConfigurer {

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}

然后 FormatResponseBodyAdvice 就可以修改为如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;


@ControllerAdvice
public class FormatResponseBodyAdvice implements ResponseBodyAdvice {

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {

if (!(body instanceof Response)) {
return new Response<>(body);
}

return body;

}
}

比之前的实现方式优雅了很多而且不用再处理 jackson 的异常了。

写一个 Controller 来尝试一下:

1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {

@GetMapping("/hello")
public String hello() {
return "hello world!";
}

}

请求这个端点得到结果:

1
2
3
4
5
{
"code": 200,
"msg": "success",
"data": "hello world!"
}

说明我们的配置是成功的,同时可以在相应头中看到:

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/

Git清理历史提交记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. Checkout

git checkout --orphan latest_branch

# 2. Add all the files

git add -A

# 3. Commit the changes

git commit -am "commit message"

# 4. Delete the branch

git branch -D master

# 5. Rename the current branch to master

git branch -m master

# 6. Finally, force update your repository

git push -f origin master

HexoClient1.3.3版本发布

更新内容

  • 加上一个启动失败的错误引导。
  • 修复好博客导航数据没有分页的问题。
  • 修改好博客导航页面布局,页面更美观。

扫码进群交流

image.png

功能预览

image.png
image.png

相关链接

HexoClient1.3.1版本发布

更新内容

  • 修复检查更新提示错误。#64
  • 修复Windows系统下的一个样式错误。#65

功能预览

image.png
image.png

相关链接

HexoClient1.3.0版本发布

更新内容

  • 修复阿里云oss图片上传后url不正确的问题。#60
  • 支持一键调用hexo generate -d命令发布文章,thanks EVINK
    image.png

功能预览

image.png
image.png

相关链接

Go 语言实战: 编写可维护 Go 语言代码建议

介绍

大家好,我在接下来的两个会议中的目标是向大家提供有关编写 Go 代码最佳实践的建议。

这是一个研讨会形式的演讲,不会有幻灯片,而是直接从文档开始。

贴士: 在这里有最新的文章链接
https://dave.cheney.net/practical-go/presentations/qcon-china.html

编者的话

  • 终于翻译完了 Dave 大神的这一篇《Go 语言最佳实践
  • 耗时两周的空闲时间
  • 翻译的同时也对 Go 语言的开发与实践有了更深层次的了解
  • 有兴趣的同学可以翻阅 Dave 的另一篇博文SOLID Go 语言设计(第六章节也会提到)
  • 同时在这里也推荐一个 Telegram Docker 群组(分享/交流): https://t.me/dockertutorial

正文

1. 指导原则

如果我要谈论任何编程语言的最佳实践,我需要一些方法来定义“什么是最佳”。如果你昨天来到我的主题演讲,你会看到 Go 团队负责人 Russ Cox 的这句话:

Software engineering is what happens to programming when you add time and other programmers. (软件工程就是你和其他程序员花费时间在编程上所发生的事情。)
— Russ Cox

Russ 作出了软件编程与软件工程的区分。 前者是你自己写的一个程序。 后者是很多人会随着时间的推移而开发的产品。 工程师们来来去去,团队会随着时间增长与缩小,需求会发生变化,功能会被添加,错误也会得到修复。 这是软件工程的本质。

我可能是这个房间里 Go 最早的用户之一,但要争辩说我的资历给我的看法更多是假的。相反,今天我要提的建议是基于我认为的 Go 语言本身的指导原则:

  1. 简单性
  2. 可读性
  3. 生产力

注意:
你会注意到我没有说性能或并发。 有些语言比 Go 语言快一点,但它们肯定不像 Go 语言那么简单。 有些语言使并发成为他们的最高目标,但它们并不具有可读性及生产力。
性能和并发是重要的属性,但不如简单性,可读性和生产力那么重要。

1.1. 简单性

我们为什么要追求简单? 为什么 Go 语言程序的简单性很重要?

我们都曾遇到过这样的情况: “我不懂这段代码”,不是吗? 我们都做过这样的项目:你害怕做出改变,因为你担心它会破坏程序的另一部分; 你不理解的部分,不知道如何修复。

这就是复杂性。 复杂性把可靠的软件中变成不可靠。 复杂性是杀死软件项目的罪魁祸首。

简单性是 Go 语言的最高目标。 无论我们编写什么程序,我们都应该同意这一点:它们很简单。

1.2. 可读性

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 语言。

如果你正在为自己编写一个程序,也许它只需要运行一次,或者你是唯一一个曾经看过它的人,然后做任何对你有用的事。但是,如果是一个不止一个人会贡献编写的软件,或者在很长一段时间内需求、功能或者环境会改变,那么你的目标必须是你的程序可被维护。

编写可维护代码的第一步是确保代码可读。

1.3. 生产力

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 团队会谈论生产力。

2. 标识符

我们要讨论的第一个主题是标识符。 标识符是一个用来表示名称的花哨单词; 变量的名称,函数的名称,方法的名称,类型的名称,包的名称等。

Poor naming is symptomatic of poor design. (命名不佳是设计不佳的症状。)
— Dave Cheney

鉴于 Go 语言的语法有限,我们为程序选择的名称对我们程序的可读性产生了非常大的影响。 可读性是良好代码的定义质量,因此选择好名称对于 Go 代码的可读性至关重要。

2.1. 选择标识符是为了清晰,而不是简洁

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 程序员在谈到习惯用语时所谈论的内容。

让我们深入讨论以下这些属性。

2.2. 标识符长度

有时候人们批评 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Person struct {
Name string
Age int
}

// AverageAge returns the average age of people.
func AverageAge(people []Person) int {
if len(people) == 0 {
return 0
}

var count, sum int
for _, p := range people {
sum += p.Age
count += 1
}

return sum / count
}

在此示例中,变量 p 的在第 10 行被声明并且也只在接下来的一行中被引用。 p 在执行函数期间存在时间很短。如果要了解 p 的作用只需阅读两行代码。

相比之下,people 在函数第 7 行参数中被声明。sumcount 也是如此,他们用了更长的名字。读者必须查看更多的行数来定位它们,因此他们名字更为独特。

我可以选择 s 替代 sum 以及 c(或可能是 n)替代 count,但是这样做会将程序中的所有变量份量降低到同样的级别。我可以选择 p 来代替 people,但是用什么来调用 for ... range 迭代变量。如果用 person 的话看起来很奇怪,因为循环迭代变量的生命时间很短,其名字的长度超出了它的值。

贴士:
与使用段落分解文档的方式一样用空行来分解函数。 在 AverageAge 中,按顺序共有三个操作。 第一个是前提条件,检查 people 是否为空,第二个是 sumcount 的累积,最后是平均值的计算。

2.2.1. 上下文是关键

重要的是要意识到关于命名的大多数建议都是需要考虑上下文的。 我想说这是一个原则,而不是一个规则。

两个标识符 iindex 之间有什么区别。 我们不能断定一个就比另一个好,例如

1
2
3
for index := 0; index < len(s); index++ {
//
}

从根本上说,上面的代码更具有可读性

1
2
3
for i := 0; i < len(s); i++ {
//
}

我认为它不是,因为就此事而论, iindex 的范围很大可能上仅限于 for 循环的主体,后者的额外冗长性(指 index)几乎没有增加对于程序的理解。

但是,哪些功能更具可读性?

1
func (s *SNMP) Fetch(oid []int, index int) (int, error)

1
func (s *SNMP) Fetch(o []int, i int) (int, error)

在此示例中,oidSNMP 对象 ID 的缩写,因此将其缩短为 o 意味着程序员必须要将文档中常用符号转换为代码中较短的符号。 类似地将 index 替换成 i,模糊了 i 所代表的含义,因为在 SNMP 消息中,每个 OID 的子值称为索引。

贴士: 在同一声明中长和短形式的参数不能混搭。

2.3. 不要用变量类型命名你的变量

你不应该用变量的类型来命名你的变量, 就像您不会将宠物命名为“狗”和“猫”。 出于同样的原因,您也不应在变量名字中包含类型的名字。

变量的名称应描述其内容,而不是内容的类型。 例如:

1
var usersMap map[string]*User

这个声明有什么好处? 我们可以看到它是一个 map,它与 *User 类型有关。 但是 usersMap 是一个 map,而 Go 语言是一种静态类型的语言,如果没有定义变量,不会让我们意外地使用到它,因此 Map 后缀是多余的。

接下来, 如果我们像这样来声明其他变量:

1
2
3
4
var (
companiesMap map[string]*Company
productsMap map[string]*Products
)

usersMapcompaniesMapproductsMap 三个 map 类型变量,所有映射字符串都是不同的类型。 我们知道它们是 map,我们也知道我们不能使用其中一个来代替另一个 - 如果我们在需要 map[string]*User 的地方尝试使用 companiesMap, 编译器将抛出错误异常。 在这种情况下,很明显变量中 Map 后缀并没有提高代码的清晰度,它只是增加了要输入的额外样板代码。

我的建议是避免使用任何类似变量类型的后缀。

贴士:
如果 users 的描述性都不够用,那么 usersMap 也不会。

此建议也适用于函数参数。 例如:

1
2
3
4
5
type Config struct {
//
}

func WriteConfig(w io.Writer, config *Config)

命名 *Config 参数 config 是多余的。 我们知道它是 *Config 类型,就是这样。

在这种情况下,如果变量的生命周期足够短,请考虑使用 confc

如果有更多的 *Config,那么将它们称为 originalupdatedconf1conf2 会更具描述性,因为前者不太可能被互相误解。

贴士:
不要让包名窃取好的变量名。
导入标识符的名称包括其包名称。 例如,context 包中的 Context 类型将被称为 context.Context。 这使得无法将 context 用作包中的变量或类型。

1
func WriteLog(context context.Context, message string)

上面的栗子将会编译出错。 这就是为什么 context.Context 类型的通常的本地声明是 ctx,例如:

1
func WriteLog(ctx context.Context, message string)

2.4. 使用一致的命名方式

一个好名字的另一个属性是它应该是可预测的。 在第一次遇到该名字时读者就能够理解名字的使用。 当他们遇到常见的名字时,他们应该能够认为自从他们上次看到它以来它没有改变意义。

例如,如果您的代码在处理数据库请确保每次出现参数时,它都具有相同的名称。 与其使用 d * sql.DBdbase * sql.DBDB * sql.DBdatabase * sql.DB 的组合,倒不如统一使用:

1
db *sql.DB

这样做使读者更为熟悉; 如果你看到db,你知道它就是 *sql.DB 并且它已经在本地声明或者由调用者为你提供。

类似地,对于方法接收器: 在该类型的每个方法上使用相同的接收者名称。 在这种类型的方法内部可以使读者更容易使用。

注意:
Go 语言中的短接收者名称惯例与目前提供的建议不一致。 这只是早期做出的选择之一,已经成为首选的风格,就像使用 CamelCase 而不是 snake_case 一样。

贴士:
Go 语言样式规定接收器具有单个字母名称或从其类型派生的首字母缩略词。 你可能会发现接收器的名称有时会与方法中参数的名称冲突。 在这种情况下,请考虑将参数名称命名稍长,并且不要忘记一致地使用此新参数名称。

最后,某些单字母变量传统上与循环和计数相关联。 例如,ijk 通常是简单 for 循环的循环归纳变量。n 通常与计数器或累加器相关联。v 是通用编码函数中值的常用简写,k 通常用于 map 的键,s 通常用作字符串类型参数的简写。

与上面的 db 示例一样,程序员认为 i 是一个循环归纳变量。 如果确保 i 始终是循环变量,而且不在 for 循环之外的其他地方中使用。 当读者遇到一个名为 ij 的变量时,他们知道循环就在附近。

贴士:
如果你发现自己有如此多的嵌套循环,ijk 变量都无法满足时,这个时候可能就是需要将函数分解成更小的函数。

2.5. 使用一致的声明样式

Go 至少有六种不同的方式来声明变量

  • var x int = 1
  • var x = 1
  • var x int; x = 1
  • var x = int(1)
  • x := 1

我确信还有更多我没有想到的。 这可能是 Go 语言的设计师意识到的一个错误,但现在改变它为时已晚。 通过所有这些不同的方式来声明变量,我们如何避免每个 Go 程序员选择自己的风格?

我想就如何在程序中声明变量提出建议。 这是我尽可能使用的风格。

  • 声明变量但没有初始化时,请使用 var 当声明变量稍后将在函数中初始化时,请使用 var 关键字。

    1
    2
    3
    4
    5
    6
    var players int    // 0

    var things []Thing // an empty slice of Things

    var thing Thing // empty Thing struct
    json.Unmarshall(reader, &thing)

    var 表示此变量已被声明为指定类型的零值。 这也与使用 var 而不是短声明语法在包级别声明变量的要求一致 - 尽管我稍后会说你根本不应该使用包级变量。

  • 在声明和初始化时,使用 := 在同时声明和初始化变量时,也就是说我们不会将变量初始化为零值,我建议使用短变量声明。 这使得读者清楚地知道 := 左侧的变量是初始化过的。

为了解释原因,让我们看看前面的例子,但这次是初始化每个变量:

1
2
3
4
5
6
var players int = 0

var things []Thing = nil

var thing *Thing = new(Thing)
json.Unmarshall(reader, thing)

在第一个和第三个例子中,因为在 Go 语言中没有从一种类型到另一种类型的自动转换; 赋值运算符左侧的类型必须与右侧的类型相同。 编译器可以从右侧的类型推断出声明的变量的类型,上面的例子可以更简洁地写为:

1
2
3
4
5
6
var players = 0

var things []Thing = nil

var thing = new(Thing)
json.Unmarshall(reader, thing)

我们将 players 初始化为 0,但这是多余的,因为 0players 的零值。 因此,要明确地表示使用零值, 我们将上面例子改写为:

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
2
var thing Thing
json.Unmarshall(reader, &thing)

贴士:
当然,任何经验法则,都有例外。 例如,有时两个变量密切相关,这样写会很奇怪:

1
2
var min int
max := 1000

如果这样声明可能更具可读性

1
min, max := 0, 1000

综上所述:

在没有初始化的情况下声明变量时,请使用 var 语法。

声明并初始化变量时,请使用 :=

贴士:
使复杂的声明显而易见。
当事情变得复杂时,它看起来就会很复杂。例如

1
var length uint32 = 0x80

这里 length 可能要与特定数字类型的库一起使用,并且 length 明确选择为 uint32 类型而不是短声明形式:

1
length := uint32(0x80)

在第一个例子中,我故意违反了规则, 使用 var 声明带有初始化变量的。 这个决定与我的常用的形式不同,这给读者一个线索,告诉他们一些不寻常的事情将会发生。

2.6. 成为团队合作者

我谈到了软件工程的目标,即编写可读及可维护的代码。 因此,您可能会将大部分职业生涯用于你不是唯一作者的项目。 我在这种情况下的建议是遵循项目自身风格。

在文件中间更改样式是不和谐的。 即使不是你喜欢的方式,对于维护而言一致性比你的个人偏好更有价值。 我的经验法则是: 如果它通过了 gofmt,那么通常不值得再做代码审查。

贴士:
如果要在代码库中进行重命名,请不要将其混合到另一个更改中。 如果有人使用 git bisect,他们不想通过数千行重命名来查找您更改的代码。

3. 注释

在我们继续讨论更大的项目之前,我想花几分钟时间谈论一下注释。

Good code has lots of comments, bad code requires lots of comments.
(好的代码有很多注释,坏代码需要很多注释。)
— Dave Thomas and Andrew Hunt (The Pragmatic Programmer)

注释对 Go 语言程序的可读性非常重要。 注释应该做的三件事中的一件:

  1. 注释应该解释其作用。
  2. 注释应该解释其如何做的。
  3. 注释应该解释其原因。

第一种形式是公共符号注释的理想选择:

1
2
// Open opens the named file for reading.
// If successful, methods on the returned file can be used for reading.

第二种形式非常适合在方法中注释:

1
2
3
4
5
// queue all dependant actions
var results []chan error
for _, dep := range a.Deps {
results = append(results, execute(seen, dep))
}

第三种形式是独一无二的,因为它不会取代前两种形式,但与此同时它并不能代替前两种形式。 此形式的注解用以解释代码的外部因素。 这些因素脱离上下文后通常很难理解,此注释的为了提供这种上下文。

1
2
3
4
5
6
return &v2.Cluster_CommonLbConfig{
// Disable HealthyPanicThreshold
HealthyPanicThreshold: &envoy_type.Percent{
Value: 0,
},
}

在此示例中,无法清楚地明白 HealthyPanicThreshold 设置为零百分比的效果。 需要注释 0 值将禁用 panic 阀值。

3.1. 关于变量和常量的注释应描述其内容而非其目的

我之前谈过,变量或常量的名称应描述其目的。 向变量或常量添加注释时,该注释应描述变量内容,而不是变量目的。

1
const randomNumber = 6 // determined from an unbiased die

在此示例中,注释描述了为什么 randomNumber 被赋值为6,以及6来自哪里。 注释没有描述 randomNumber 的使用位置。 还有更多的栗子:

1
2
3
4
5
6
const (
StatusContinue = 100 // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
StatusProcessing = 102 // RFC 2518, 10.1

StatusOK = 200 // RFC 7231, 6.3.1

在HTTP的上下文中,数字 100 被称为 StatusContinue,如 RFC 7231 第 6.2.1 节中所定义。

贴士:
对于没有初始值的变量,注释应描述谁负责初始化此变量。

1
2
3
// sizeCalculationDisabled indicates whether it is safe
// to calculate Types' widths and alignments. See dowidth.
var sizeCalculationDisabled bool

这里的注释让读者知道 dowidth 函数负责维护 sizeCalculationDisabled 的状态。

隐藏在众目睽睽下
这个提示来自Kate Gregory[3]。有时你会发现一个更好的变量名称隐藏在注释中。

1
2
// registry of SQL drivers
var registry = make(map[string]*sql.Driver)

注释是由作者添加的,因为 registry 没有充分解释其目的 - 它是一个注册表,但注册的是什么?

通过将变量重命名为 sqlDrivers,现在可以清楚地知道此变量的目的是保存SQL驱动程序。

1
var sqlDrivers = make(map[string]*sql.Driver)

之前的注释就是多余的,可以删除。

3.2. 公共符号始终要注释

godoc 是包的文档,所以应该始终为包中声明的每个公共符号 —​ 变量、常量、函数以及方法添加注释。

以下是 Google Style 指南中的两条规则:

  • 任何既不明显也不简短的公共功能必须予以注释。
  • 无论长度或复杂程度如何,对库中的任何函数都必须进行注释
    1
    2
    3
    4
    5
    6
    7
    package ioutil

    // ReadAll reads from r until an error or EOF and returns the data it read.
    // A successful call returns err == nil, not err == EOF. Because ReadAll is
    // defined to read from src until EOF, it does not treat an EOF from Read
    // as an error to be reported.
    func ReadAll(r io.Reader) ([]byte, error)
    这条规则有一个例外; 您不需要注释实现接口的方法。 具体不要像下面这样做:
    1
    2
    // Read implements the io.Reader interface
    func (r *FileReader) Read(buf []byte) (int, error)
    这个注释什么也没说。 它没有告诉你这个方法做了什么,更糟糕是它告诉你去看其他地方的文档。 在这种情况下,我建议完全删除该注释。

这是 io 包中的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// LimitReader returns a Reader that reads from r
// but stops with EOF after n bytes.
// The underlying implementation is a *LimitedReader.
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }

// A LimitedReader reads from R but limits the amount of
// data returned to just N bytes. Each call to Read
// updates N to reflect the new amount remaining.
// Read returns EOF when N <= 0 or when the underlying R returns EOF.
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}

func (l *LimitedReader) Read(p []byte) (n int, err error) {
if l.N <= 0 {
return 0, EOF
}
if int64(len(p)) > l.N {
p = p[0:l.N]
}
n, err = l.R.Read(p)
l.N -= int64(n)
return
}

请注意,LimitedReader 的声明就在使用它的函数之前,而 LimitedReader.Read 的声明遵循 LimitedReader 本身的声明。 尽管 LimitedReader.Read 本身没有文档,但它清楚地表明它是 io.Reader 的一个实现。

贴士:
在编写函数之前,请编写描述函数的注释。 如果你发现很难写出注释,那么这就表明你将要编写的代码很难理解。

3.2.1. 不要注释不好的代码,将它重写

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 与日期或问题编号来注释。

3.2.2. 与其注释一段代码,不如重构它

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

函数应该只做一件事。 如果你发现自己在注释一段与函数的其余部分无关的代码,请考虑将其提取到它自己的函数中。

除了更容易理解之外,较小的函数更易于隔离测试,将代码隔离到函数中,其名称可能是所需的所有文档。

4. 包的设计

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 语言包应该具有低程度的源码级耦合,这样,随着项目的增长,对一个包的更改不会跨代码库级联。 这些世界末日的重构严格限制了代码库的变化率以及在该代码库中工作的成员的生产率。

在本节中,我们将讨论如何设计包,包括包的名称,命名类型以及编写方法和函数的技巧。

4.1. 一个好的包从它的名字开始

编写一个好的 Go 语言包从包的名称开始。将你的包名用一个词来描述它。

正如我在上一节中谈到变量的名称一样,包的名称也非常重要。我遵循的经验法则不是“我应该在这个包中放入什么类型的?”。相反,我要问是“该包提供的服务是什么?”通常这个问题的答案不是“这个包提供 X 类型”,而是“这个包提供 HTTP”。

贴士:
以包所提供的内容来命名,而不是它包含的内容。

4.1.1. 好的包名应该是唯一的。

在项目中,每个包名称应该是唯一的。包的名称应该描述其目的的建议很容易理解 - 如果你发现有两个包需要用相同名称,它可能是:

  1. 包的名称太通用了。
  2. 该包与另一个类似名称的包重叠了。在这种情况下,您应该检查你的设计,或考虑合并包。

4.2. 避免使用类似 basecommonutil 的包名称

不好的包名的常见情况是 utility 包。这些包通常是随着时间的推移一些帮助程序和工具类的包。由于这些包包含各种不相关的功能,因此很难根据包提供的内容来描述它们。这通常会导致包的名称来自包含的内容 - utilities

utilshelper 这样的包名称通常出现在较大的项目中,这些项目已经开发了深层次包的结构,并且希望在不遇到导入循环的情况下共享 helper 函数。通过将 utility 程序函数提取到新的包中,导入循环会被破坏,但由于该包源于项目中的设计问题,因此其包名称不反映其目的,仅反映其为了打破导入循环。

我建议改进 utilshelpers 包的名称是分析它们的调用位置,如果可能的话,将相关的函数移动到调用者的包中。即使这涉及复制一些 helper 程序代码,这也比在两个程序包之间引入导入依赖项更好。

[A little] duplication is far cheaper than the wrong abstraction.
([一点点]重复比错误的抽象的性价比高很多。)
— Sandy Metz

在使用 utility 程序的情况下,最好选多个包,每个包专注于单个方面,而不是选单一的整体包。

贴士:
使用复数形式命名 utility 包。例如 strings 来处理字符串。

当两个或多个实现共有的功能或客户端和服务器的常见类型被重构为单独的包时,通常会找到名称类似于 basecommon 的包。我相信解决方案是减少包的数量,将客户端,服务器和公共代码组合到一个以包的功能命名的包中。

例如,net/http 包没有 clientserver 的分包,而是有一个 client.goserver.go 文件,每个文件都有各自的类型,还有一个 transport.go 文件,用于公共消息传输代码。

贴士:
标识符的名称包括其包名称。
重要的是标识符的名称包括其包的名称。

  • 当由另一个包引用时,net/http 包中的 Get 函数变为 http.Get
  • 当导入到其他包中时,strings 包中的 Reader 类型变为 strings.Reader
  • net 包中的 Error 接口显然与网络错误有关。

4.3. 尽早 return 而不是深度嵌套

由于 Go 语言的控制流不使用 exception,因此不需要为 trycatch 块提供顶级结构而深度缩进代码。Go 语言代码不是成功的路径越来越深地嵌套到右边,而是以一种风格编写,其中随着函数的进行,成功路径继续沿着屏幕向下移动。 我的朋友 Mat Ryer 将这种做法称为“视线”编码。[4]

这是通过使用 guard clauses 来实现的; 在进入函数时是具有断言前提条件的条件块。 这是一个来自 bytes 包的例子:

1
2
3
4
5
6
7
8
9
10
func (b *Buffer) UnreadRune() error {
if b.lastRead <= opInvalid {
return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune")
}
if b.off >= int(b.lastRead) {
b.off -= int(b.lastRead)
}
b.lastRead = opInvalid
return nil
}

进入 UnreadRune 后,将检查 b.lastRead 的状态,如果之前的操作不是 ReadRune,则会立即返回错误。 之后,函数的其余部分继续进行 b.lastRead 大于 opInvalid 的断言。

与没有 guard clause 的相同函数进行比较,

1
2
3
4
5
6
7
8
9
10
func (b *Buffer) UnreadRune() error {
if b.lastRead > opInvalid {
if b.off >= int(b.lastRead) {
b.off -= int(b.lastRead)
}
b.lastRead = opInvalid
return nil
}
return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune")
}

最常见的执行成功的情况是嵌套在第一个if条件内,成功的退出条件是 return nil,而且必须通过仔细匹配大括号来发现。 函数的最后一行是返回一个错误,并且被调用者必须追溯到匹配的左括号,以了解何时执行到此点。

对于读者和维护程序员来说,这更容易出错,因此 Go 语言更喜欢使用 guard clauses 并尽早返回错误。

4.4. 让零值更有用

假设变量没有初始化,每个变量声明都会自动初始化为与零内存的内容相匹配的值。 这就是零值。 值的类型决定了其零值; 对于数字类型,它为 0,对于指针类型为 nilslicesmapchannel 同样是 nil

始终设置变量为已知默认值的属性对于程序的安全性和正确性非常重要,并且可以使 Go 语言程序更简单、更紧凑。 这就是 Go 程序员所说的“给你的结构一个有用的零值”。

对于 sync.Mutex 类型。sync.Mutex 包含两个未公开的整数字段,它们用来表示互斥锁的内部状态。 每当声明 sync.Mutex 时,其字段会被设置为 0 初始值。sync.Mutex 利用此属性来编写,使该类型可直接使用而无需初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
type MyInt struct {
mu sync.Mutex
val int
}

func main() {
var i MyInt

// i.mu is usable without explicit initialisation.
i.mu.Lock()
i.val++
i.mu.Unlock()
}

另一个利用零值的类型是 bytes.Buffer。您可以声明 bytes.Buffer 然后就直接写入而无需初始化。

1
2
3
4
5
func main() {
var b bytes.Buffer
b.WriteString("Hello, world!\n")
io.Copy(os.Stdout, &b)
}

切片的一个有用属性是它们的零值 nil。如果我们看一下切片运行时 header 的定义就不难理解:

1
2
3
4
5
type slice struct {
array *[...]T // pointer to the underlying array
len int
cap int
}

此结构的零值意味着 lencap 的值为 0,而 array(指向保存切片的内容数组的指针)将为 nil。这意味着你不需要 make 切片,你只需声明它即可。

1
2
3
4
5
6
7
8
9
func main() {
// s := make([]string, 0)
// s := []string{}
var s []string

s = append(s, "Hello")
s = append(s, "world")
fmt.Println(strings.Join(s, " "))
}

注意:
var s []string 类似于它上面的两条注释行,但并不完全相同。值为 nil 的切片与具有零长度的切片就可以来相互比较。以下代码将输出 false

1
2
3
4
5
func main() {
var s1 = []string{}
var s2 []string
fmt.Println(reflect.DeepEqual(s1, s2))
}

nil pointers – 未初始化的指针变量的一个有用属性是你可以在具有 nil 值的类型上调用方法。它可以简单地用于提供默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Config struct {
path string
}

func (c *Config) Path() string {
if c == nil {
return "/usr/home"
}
return c.path
}

func main() {
var c1 *Config
var c2 = &Config{
path: "/export",
}
fmt.Println(c1.Path(), c2.Path())
}

4.5. 避免包级别状态

编写可维护程序的关键是它们应该是松散耦合的 - 对一个程序包的更改应该很少影响另一个不直接依赖于第一个程序包的程序包。

在 Go 语言中有两种很好的方法可以实现松散耦合

  1. 使用接口来描述函数或方法所需的行为。
  2. 避免使用全局状态。

在 Go 语言中,我们可以在函数或方法范围以及包范围内声明变量。当变量是公共的时,给定一个以大写字母开头的标识符,那么它的范围对于整个程序来说实际上是全局的 - 任何包都可以随时观察该变量的类型和内容。

可变全局状态引入程序的独立部分之间的紧密耦合,因为全局变量成为程序中每个函数的不可见参数!如果该变量的类型发生更改,则可以破坏依赖于全局变量的任何函数。如果程序的另一部分更改了该变量,则可以破坏依赖于全局变量状态的任何函数。

如果要减少全局变量所带来的耦合,

  1. 将相关变量作为字段移动到需要它们的结构上。
  2. 使用接口来减少行为与实现之间的耦合。

5. 项目结构

我们来谈谈如何将包组合到项目中。 通常一个项目是一个 git 仓库,但在未来 Go 语言开发人员会交替地使用 moduleproject

就像一个包,每个项目都应该有一个明确的目的。 如果你的项目是一个库,它应该提供一件事,比如 XML 解析或记录。 您应该避免在一个包实现多个目的,这将有助于避免成为 common 库。

贴士:
据我的经验,common 库最终会与其最大的调用者紧密相连,在没有升级该库与最大调用者的情况下是很难修复的,还会带来了许多无关的更改以及API破坏。

如果你的项目是应用程序,如 Web 应用程序,Kubernetes 控制器等,那么项目中可能有一个或多个 main 程序包。 例如,我编写的 Kubernetes 控制器有一个 cmd/contour 包,既可以作为部署到 Kubernetes 集群的服务器,也可以作为调试目的的客户端。

5.1. 考虑更少,更大的包

对于从其他语言过渡到 Go 语言的程序员来说,我倾向于在代码审查中提到的一件事是他们会过度使用包。

Go 语言没有提供有关可见性的详细方法; Java有 publicprotectedprivate 以及隐式 default 的访问修饰符。 没有 C++friend 类概念。

在 Go 语言中,我们只有两个访问修饰符,publicprivate,由标识符的第一个字母的大小写表示。 如果标识符是公共的,则其名称以大写字母开头,该标识符可用于任何其他 Go 语言包的引用。

注意:
你可能会听到人们说 exportednot exported, 跟 publicprivate 是同义词。

鉴于包的符号的访问有限控件,Go 程序员应遵循哪些实践来避免创建过于复杂的包层次结构?

贴士:
cmd/internal/ 之外的每个包都应包含一些源代码。

我的建议是选择更少,更大的包。 你应该做的是不创建新的程序包。 这将导致太多类型被公开,为你的包创建一个宽而浅的API。

以下部分将更为详细地探讨这一建议。

贴士:
来自 Java
如果您来自 JavaC#,请考虑这一经验法则 – Java 包相当于单个 .go 源文件。 - Go 语言包相当于整个 Maven 模块或 .NET 程序集。

5.1.1. 通过 import 语句将代码排列到文件中

如果你按照包提供的内容来安排你的程序包,是否需要对 Go 包中的文件也执行相同的操作?什么时候应该将 .go 文件拆分成多个文件?什么时候应该考虑整合 .go 文件?

以下是我的经验法则:

  • 开始时使用一个 .go 文件。为该文件指定与文件夹名称相同的名称。例如: package http 应放在名为 http 的目录中名为 http.go 的文件中。
  • 随着包的增长,您可能决定将各种职责任务拆分为不同的文件。例如:messages.go 包含 RequestResponse 类型,client.go 包含 Client 类型,server.go包含 Server 类型。
  • 如果你的文件中 import 的声明类似,请考虑将它们组合起来。或者确定 import 集之间的差异并移动它们。
  • 不同的文件应该负责包的不同区域。messages.go 可能负责网络的 HTTP 请求和响应,http.go 可能包含底层网络处理逻辑,client.goserver.go 实现 HTTP 业务逻辑请求的实现或路由等等。

贴士: 首选名词为源文件命名。

注意:
Go编译器并行编译每个包。 在一个包中,编译器并行编译每个函数(方法只是 Go 语言中函数的另一种写法)。 更改包中代码的布局不会影响编译时间。

5.1.2. 优先内部测试再到外部测试

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 文件的中间目录,则可能无法遵循此建议。

5.1.3. 使用 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]

5.2. 确保 main 包内容尽可能的少

main 函数和 main 包的内容应尽可能少。 这是因为 main.main 充当单例; 程序中只能有一个 main 函数,包括 tests

因为 main.main 是一个单例,假设 main 函数中需要执行很多事情,main.main 只会在 main.mainmain.init 中调用它们并且只调用一次。 这使得为 main.main 编写代码测试变得很困难,因此你应该将所有业务逻辑从 main 函数中移出,最好是从 main 包中移出。

贴士:
main 应该做解析 flags,开启数据库连接、开启日志等,然后将执行交给更高一级的对象。

6. API 设计

我今天要给出的最后一条建议是设计, 我认为也是最重要的。

到目前为止我提出的所有建议都是建议。 这些是我尝试编写 Go 语言的方式,但我不打算在代码审查中拼命推广。

但是,在审查 API 时, 我就不会那么宽容了。 这是因为到目前为止我所谈论的所有内容都是可以修复而且不会破坏向后兼容性; 它们在很大程度上是实现的细节。

当涉及到软件包的公共 API 时,在初始设计中投入大量精力是值得的,因为稍后更改该设计对于已经使用 API 的人来说会是破坏性的。

6.1. 设计难以被误用的 API

APIs should be easy to use and hard to misuse.
(API 应该易于使用且难以被误用)
— Josh Bloch [3]

如果你从这个演讲中带走任何东西,那应该是 Josh Bloch 的建议。 如果一个 API 很难用于简单的事情,那么 API 的每次调用都会很复杂。 当 API 的实际调用很复杂时,它就会便得不那么明显,而且会更容易被忽视。

6.1.1. 警惕采用几个相同类型参数的函数

简单, 但难以正确使用的 API 是采用两个或更多相同类型参数的 API。 让我们比较两个函数签名:

1
2
func Max(a, b int) int
func CopyFile(to, from string) error

这两个函数有什么区别? 显然,一个返回两个数字最大的那个,另一个是复制文件,但这不重要。

1
2
Max(8, 10) // 10
Max(10, 8) // 10

Max 是可交换的; 参数的顺序无关紧要。 无论是 8 比 10 还是 10 比 8,最大的都是 10。

但是,却不适用于 CopyFile

1
2
CopyFile("/tmp/backup", "presentation.md")
CopyFile("presentation.md", "/tmp/backup")

这些声明中哪一个备份了 presentation.md,哪一个用上周的版本覆盖了 presentation.md? 没有文档,你无法分辨。 如果没有查阅文档,代码审查员也无法知道你写对了顺序。

一种可能的解决方案是引入一个 helper 类型,它会负责如何正确地调用 CopyFile

1
2
3
4
5
6
7
8
9
10
type Source string

func (src Source) CopyTo(dest string) error {
return CopyFile(dest, string(src))
}

func main() {
var from Source = "presentation.md"
from.CopyTo("/tmp/backup")
}

通过这种方式,CopyFile 总是能被正确调用 - 还可以通过单元测试 - 并且可以被设置为私有,进一步降低了误用的可能性。

贴士: 具有多个相同类型参数的API难以正确使用。

6.2. 为其默认用例设计 API

几年前,我就对 functional options[7] 进行过讨论[6],使 API 更易用于默认用例。

本演讲的主旨是你应该为常见用例设计 API。 另一方面, API 不应要求调用者提供他们不在乎参数。

6.2.1. 不鼓励使用 nil 作为参数

本章开始时我建议是不要强迫提供给 API 的调用者他们不在乎的参数。 这就是我要说的为默认用例设计 API。

这是 net/http 包中的一个例子

1
2
3
4
5
6
7
8
9
10
package http

// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {

ListenAndServe 有两个参数,一个用于监听传入连接的 TCP 地址,另一个用于处理 HTTP 请求的 http.HandlerServe 允许第二个参数为 nil,需要注意的是调用者通常会传递 nil,表示他们想要使用 http.DefaultServeMux 作为隐含参数。

现在,Serve 的调用者有两种方式可以做同样的事情。

1
2
http.ListenAndServe("0.0.0.0:8080", nil)
http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)

两者完全相同。

这种 nil 行为是病毒式的。 http 包也有一个 http.Serve 帮助类,你可以合理地想象一下 ListenAndServe 是这样构建的

1
2
3
4
5
6
7
8
func ListenAndServe(addr string, handler Handler) error {
l, err := net.Listen("tcp", addr)
if err != nil {
return err
}
defer l.Close()
return Serve(l, handler)
}

因为 ListenAndServe 允许调用者为第二个参数传递 nil,所以 http.Serve 也支持这种行为。 事实上,http.Serve 实现了如果 handlernil,使用 DefaultServeMux 的逻辑。 参数可为 nil 可能会导致调用者认为他们可以为两个参数都使用 nil。 像下面这样:

1
http.Serve(nil, nil)

会导致 panic

贴士:
不要在同一个函数签名中混合使用可为 nil 和不能为 nil 的参数。

http.ListenAndServe 的作者试图在常见情况下让使用 API 的用户更轻松些,但很可能会让该程序包更难以被安全地使用。

使用 DefaultServeMux 或使用 nil 没有什么区别。

1
2
3
const root = http.Dir("/htdocs")
http.Handle("/", http.FileServer(root))
http.ListenAndServe("0.0.0.0:8080", nil)

对比

1
2
3
const root = http.Dir("/htdocs")
http.Handle("/", http.FileServer(root))
http.ListenAndServe("0.0.0.0:8080", http.DefaultServeMux)

这种混乱值得拯救吗?

1
2
3
4
const root = http.Dir("/htdocs")
mux := http.NewServeMux()
http.Handle("/", http.FileServer(root))
http.ListenAndServe("0.0.0.0:8080", mux)

贴士: 认真考虑 helper 函数会节省不少时间。 清晰要比简洁好。

贴士:
避免公共 API 使用测试参数
避免在公开的 API 上使用仅在测试范围上不同的值。 相反,使用 Public wrappers 隐藏这些参数,使用辅助方式来设置测试范围中的属性。

6.2.2. 首选可变参数函数而非 []T 参数

编写一个带有切片参数的函数或方法是很常见的。

1
func ShutdownVMs(ids []string) error

这只是我编的一个例子,但它与我所写的很多代码相同。 这里的问题是他们假设他们会被调用于多个条目。 但是很多时候这些类型的函数只用一个参数调用,为了满足函数参数的要求,它必须打包到一个切片内。

另外,因为 ids 参数是切片,所以你可以将一个空切片或 nil 传递给该函数,编译也没什么错误。 但是这会增加额外的测试负载,因为你应该涵盖这些情况在测试中。

举一个这类 API 的例子,最近我重构了一条逻辑,要求我设置一些额外的字段,如果一组参数中至少有一个非零。 逻辑看起来像这样:

1
2
3
if svc.MaxConnections > 0 || svc.MaxPendingRequests > 0 || svc.MaxRequests > 0 || svc.MaxRetries > 0 {
// apply the non zero parameters
}

由于 if 语句变得很长,我想将签出的逻辑拉入其自己的函数中。 这就是我提出的:

1
2
3
4
5
6
7
8
9
// anyPostive indicates if any value is greater than zero.
func anyPositive(values ...int) bool {
for _, v := range values {
if v > 0 {
return true
}
}
return false
}

这就能够向读者明确内部块的执行条件:

1
2
3
if anyPositive(svc.MaxConnections, svc.MaxPendingRequests, svc.MaxRequests, svc.MaxRetries) {
// apply the non zero parameters
}

但是 anyPositive 还存在一个问题,有人可能会这样调用它:

1
if anyPositive() { ... }

在这种情况下,anyPositive 将返回 false,因为它不会执行迭代而是立即返回 false。对比起如果 anyPositive 在没有传递参数时返回 true, 这还不算世界上最糟糕的事情。

然而,如果我们可以更改 anyPositive 的签名以强制调用者应该传递至少一个参数,那会更好。我们可以通过组合正常和可变参数来做到这一点,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
// anyPostive indicates if any value is greater than zero.
func anyPositive(first int, rest ...int) bool {
if first > 0 {
return true
}
for _, v := range rest {
if v > 0 {
return true
}
}
return false
}

现在不能使用少于一个参数来调用 anyPositive

6.3. 让函数定义它们所需的行为

假设我需要编写一个将 Document 结构保存到磁盘的函数的任务。

1
2
// Save writes the contents of doc to the file f.
func Save(f *os.File, doc *Document) error

我可以指定这个函数 Save,它将 *os.File 作为写入 Document 的目标。但这样做会有一些问题

Save 的签名排除了将数据写入网络位置的选项。假设网络存储可能在以后成为需求,则此功能的签名必须改变,从而影响其所有调用者。

Save 测试起来也很麻烦,因为它直接操作磁盘上的文件。因此,为了验证其操作,测试时必须在写入文件后再读取该文件的内容。

而且我必须确保 f 被写入临时位置并且随后要将其删除。

*os.File 还定义了许多与 Save 无关的方法,比如读取目录并检查路径是否是符号链接。 如果 Save 函数的签名只用 *os.File 的相关内容,那将会很有用。

我们能做什么 ?

1
2
3
// Save writes the contents of doc to the supplied
// ReadWriterCloser.
func Save(rwc io.ReadWriteCloser, doc *Document) error

使用 io.ReadWriteCloser,我们可以应用接口隔离原则来重新定义 Save 以获取更通用文件形式。

通过此更改,任何实现 io.ReadWriteCloser 接口的类型都可以替换以前的 *os.File

这使 Save 在其应用程序中更广泛,并向 Save 的调用者阐明 *os.File 类型的哪些方法与其操作有关。

而且,Save 的作者也不可以在 *os.File 上调用那些不相关的方法,因为它隐藏在 io.ReadWriteCloser 接口后面。

但我们可以进一步采用接口隔离原则

首先,如果 Save 遵循单一功能原则,它不可能读取它刚刚写入的文件来验证其内容 - 这应该是另一段代码的功能。

1
2
3
// Save writes the contents of doc to the supplied
// WriteCloser.
func Save(wc io.WriteCloser, doc *Document) error

因此,我们可以将我们传递给 Save 的接口的规范缩小到只写和关闭。

其次,通过向 Save 提供一个关闭其流的机制,使其看起来仍然像一个文件,这就提出了在什么情况下关闭 wc 的问题。

可能 Save 会无条件地调用 Close,或者在成功的情况下调用 Close

这给 Save 的调用者带来了问题,因为它可能希望在写入文档后将其他数据写入流。

1
2
3
// Save writes the contents of doc to the supplied
// Writer.
func Save(w io.Writer, doc *Document) error

一个更好的解决方案是重新定义 Save 仅使用 io.Writer,它只负责将数据写入流。

接口隔离原则应用于我们的 Save 功能,同时, 就需求而言, 得出了最具体的一个函数 - 它只需要一个可写的东西 - 并且它的功能最通用,现在我们可以使用 Save 将我们的数据保存到实现 io.Writer 的任何事物中。

[译注: 不理解设计原则部分的同学可以阅读 Dave 大神的另一篇《Go 语言 SOLID 设计》]

7. 错误处理

我已经给出了几个关于错误处理的演示文稿[8],并在我的博客上写了很多关于错误处理的文章。我在昨天的会议上也讲了很多关于错误处理的内容,所以在这里不再赘述。

相反,我想介绍与错误处理相关的两个其他方面。

7.1. 通过消除错误来消除错误处理

如果你昨天在我的演讲中,我谈到了改进错误处理的提案。但是你知道有什么比改进错误处理的语法更好吗?那就是根本不需要处理错误。

注意:
我不是说“删除你的错误处理”。我的建议是,修改你的代码,这样就不用处理错误了。

本节从 John Ousterhout 最近的著作“软件设计哲学”[9]中汲取灵感。该书的其中一章是“定义不存在的错误”。我们将尝试将此建议应用于 Go 语言。

7.1.1. 计算行数

让我们编写一个函数来计算文件中的行数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func CountLines(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
)

for {
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
}

if err != io.EOF {
return 0, err
}
return lines, nil
}

由于我们遵循前面部分的建议,CountLines 需要一个 io.Reader,而不是一个 *File;它的任务是调用者为我们想要计算的内容提供 io.Reader

我们构造一个 bufio.Reader,然后在一个循环中调用 ReadString 方法,递增计数器直到我们到达文件的末尾,然后我们返回读取的行数。

至少这是我们想要编写的代码,但是这个函数由于需要错误处理而变得更加复杂。 例如,有这样一个奇怪的结构:

1
2
3
4
5
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}

我们在检查错误之前增加了行数,这样做看起来很奇怪。

我们必须以这种方式编写它的原因是,如果在遇到换行符之前就读到文件结束,则 ReadString 将返回错误。如果文件中没有换行符,同样会出现这种情况。

为了解决这个问题,我们重新排列逻辑增来加行数,然后查看是否需要退出循环。

注意:
这个逻辑仍然不完美,你能发现错误吗?

但是我们还没有完成检查错误。当 ReadString 到达文件末尾时,预期它会返回 io.EOFReadString 需要某种方式在没有什么可读时来停止。因此,在我们将错误返回给 CountLine 的调用者之前,我们需要检查错误是否是 io.EOF,如果不是将其错误返回,否则我们返回 nil 说一切正常。

我认为这是 Russ Cox 观察到错误处理可能会模​​糊函数操作的一个很好的例子。我们来看一个改进的版本。

1
2
3
4
5
6
7
8
9
func CountLines(r io.Reader) (int, error) {
sc := bufio.NewScanner(r)
lines := 0

for sc.Scan() {
lines++
}
return lines, sc.Err()
}

这个改进的版本从 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,而不会遇到其他错误。

贴士:
当遇到难以忍受的错误处理时,请尝试将某些操作提取到辅助程序类型中。

7.1.2. WriteResponse

我的第二个例子受到了 Errors are values 博客文章[10]的启发。

在本章前面我们已经看过处理打开、写入和关闭文件的示例。错误处理是存在的,但是接收范围内的,因为操作可以封装在诸如 ioutil.ReadFileioutil.WriteFile 之类的辅助程序中。但是,在处理底层网络协议时,有必要使用 I/O 原始的错误处理来直接构建响应,这样就可能会变得重复。看一下构建 HTTP 响应的 HTTP 服务器的这个片段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type Header struct {
Key, Value string
}

type Status struct {
Code int
Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
_, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
if err != nil {
return err
}

for _, h := range headers {
_, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
if err != nil {
return err
}
}

if _, err := fmt.Fprint(w, "\r\n"); err != nil {
return err
}

_, err = io.Copy(w, body)
return err
}

首先,我们使用 fmt.Fprintf 构造状态码并检查错误。 然后对于每个标题,我们写入键值对,每次都检查错误。 最后,我们使用额外的 \r\n 终止标题部分,检查错误之后将响应主体复制到客户端。 最后,虽然我们不需要检查 io.Copy 中的错误,但我们需要将 io.Copy 返回的两个返回值形式转换为 WriteResponse 的单个返回值。

这里很多重复性的工作。 我们可以通过引入一个包装器类型 errWriter 来使其更容易。

errWriter 实现 io.Writer 接口,因此可用于包装现有的 io.WritererrWriter 写入传递给其底层 writer,直到检测到错误。 从此时起,它会丢弃任何写入并返回先前的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
type errWriter struct {
io.Writer
err error
}

func (e *errWriter) Write(buf []byte) (int, error) {
if e.err != nil {
return 0, e.err
}
var n int
n, e.err = e.Writer.Write(buf)
return n, nil
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
ew := &errWriter{Writer: w}
fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

for _, h := range headers {
fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
}

fmt.Fprint(ew, "\r\n")
io.Copy(ew, body)
return ew.err
}

errWriter 应用于 WriteResponse 可以显着提高代码的清晰度。 每个操作不再需要自己做错误检查。 通过检查 ew.err 字段,将错误报告移动到函数末尾,从而避免转换从 io.Copy 的两个返回值。

7.2. 错误只处理一次

最后,我想提一下你应该只处理错误一次。 处理错误意味着检查错误值并做出单一决定。

1
2
3
4
// WriteAll writes the contents of buf to the supplied writer.
func WriteAll(w io.Writer, buf []byte) {
w.Write(buf)
}

如果你做出的决定少于一个,则忽略该错误。 正如我们在这里看到的那样, w.WriteAll 的错误被丢弃。

但是,针对单个错误做出多个决策也是有问题的。 以下是我经常遇到的代码。

1
2
3
4
5
6
7
8
func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
log.Println("unable to write:", err) // annotated error goes to log file
return err // unannotated error returned to caller
}
return nil
}

在此示例中,如果在 w.Write 期间发生错误,则会写入日志文件,注明错误发生的文件与行数,并且错误也会返回给调用者,调用者可能会记录该错误并将其返回到上一级,一直回到程序的顶部。

调用者可能正在做同样的事情

1
2
3
4
5
6
7
8
9
10
11
12
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
return err
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}

因此你在日志文件中得到一堆重复的内容,

1
2
unable to write: io.EOF
could not write config: io.EOF

但在程序的顶部,虽然得到了原始错误,但没有相关内容。

1
2
err := WriteConfig(f, &conf)
fmt.Println(err) // io.EOF

我想深入研究这一点,因为作为个人偏好, 我并没有看到 logging 和返回的问题。

1
2
3
4
5
6
7
8
9
10
11
12
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
log.Printf("could not marshal config: %v", err)
// oops, forgot to return
}
if err := WriteAll(w, buf); err != nil {
log.Println("could not write config: %v", err)
return err
}
return nil
}

很多问题是程序员忘记从错误中返回。正如我们之前谈到的那样,Go 语言风格是使用 guard clauses 以及检查前提条件作为函数进展并提前返回。

在这个例子中,作者检查了错误,记录了它,但忘了返回。这就引起了一个微妙的错误。

Go 语言中的错误处理规定,如果出现错误,你不能对其他返回值的内容做出任何假设。由于 JSON 解析失败,buf 的内容未知,可能它什么都没有,但更糟的是它可能包含解析的 JSON 片段部分。

由于程序员在检查并记录错误后忘记返回,因此损坏的缓冲区将传递给 WriteAll,这可能会成功,因此配置文件将被错误地写入。但是,该函数会正常返回,并且发生问题的唯一日志行是有关 JSON 解析错误,而与写入配置失败有关。

7.2.1. 为错误添加相关内容

发生错误的原因是作者试图在错误消息中添加 context 。 他们试图给自己留下一些线索,指出错误的根源。

让我们看看使用 fmt.Errorf 的另一种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func WriteConfig(w io.Writer, conf *Config) error {
buf, err := json.Marshal(conf)
if err != nil {
return fmt.Errorf("could not marshal config: %v", err)
}
if err := WriteAll(w, buf); err != nil {
return fmt.Errorf("could not write config: %v", err)
}
return nil
}

func WriteAll(w io.Writer, buf []byte) error {
_, err := w.Write(buf)
if err != nil {
return fmt.Errorf("write failed: %v", err)
}
return nil
}

通过将注释与返回的错误组合起来,就更难以忘记错误的返回来避免意外继续。

如果写入文件时发生 I/O 错误,则 errorError() 方法会报告以下类似的内容;

1
could not write config: write failed: input/output error

7.2.2. 使用 github.com/pkg/errors 包装 errors

fmt.Errorf 模式适用于注释错误 message,但这样做的代价是模糊了原始错误的类型。 我认为将错误视为不透明值对于松散耦合的软件非常重要,因此如果你使用错误值做的唯一事情是原始错误的类型应该无关紧要的面孔

  1. 检查它是否为 nil
  2. 输出或记录它。

但是在某些情况下,我认为它们并不常见,您需要恢复原始错误。 在这种情况下,使用类似我的 errors 包来注释这样的错误, 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()

buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, errors.Wrap(err, "read failed")
}
return buf, nil
}

func ReadConfig() ([]byte, error) {
home := os.Getenv("HOME")
config, err := ReadFile(filepath.Join(home, ".settings.xml"))
return config, errors.WithMessage(err, "could not read config")
}

func main() {
_, err := ReadConfig()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

现在报告的错误就是 K&D [11]样式错误,

1
could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory

并且错误值保留对原始原因的引用。

1
2
3
4
5
6
7
8
func main() {
_, err := ReadConfig()
if err != nil {
fmt.Printf("original error: %T %v\n", errors.Cause(err), errors.Cause(err))
fmt.Printf("stack trace:\n%+v\n", err)
os.Exit(1)
}
}

因此,你可以恢复原始错误并打印堆栈跟踪;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
original error: *os.PathError open /Users/dfc/.settings.xml: no such file or directory
stack trace:
open /Users/dfc/.settings.xml: no such file or directory
open failed
main.ReadFile
/Users/dfc/devel/practical-go/src/errors/readfile2.go:16
main.ReadConfig
/Users/dfc/devel/practical-go/src/errors/readfile2.go:29
main.main
/Users/dfc/devel/practical-go/src/errors/readfile2.go:35
runtime.main
/Users/dfc/go/src/runtime/proc.go:201
runtime.goexit
/Users/dfc/go/src/runtime/asm_amd64.s:1333
could not read config

使用 errors 包,你可以以人和机器都可检查的方式向错误值添加上下文。 如果昨天你来听我的演讲,你会知道这个库在被移植到即将发布的 Go 语言版本的标准库中。

8. 并发

由于 Go 语言的并发功能,经常被选作项目编程语言。 Go 语言团队已经竭尽全力以廉价(在硬件资源方面)和高性能来实现并发,但是 Go 语言的并发功能也可以被用来编写性能不高同时也不太可靠的代码。在结尾,我想留下一些建议,以避免 Go 语言的并发功能带来的一些陷阱。

Go 语言以 channels 以及 selectgo 语句来支持并发。如果你已经从书籍或培训课程中正式学习了 Go 语言,你可能已经注意到并发部分始终是这些课程的最后一部分。这个研讨会也没有什么不同,我选择最后覆盖并发,好像它是 Go 程序员应该掌握的常规技能的额外补充。

这里有一个二分法; Go 语言的最大特点是简单、轻量级的并发模型。作为一种产品,我们的语言几乎只推广这个功能。另一方面,有一种说法认为并发使用起来实际上并不容易,否则作者不会把它作为他们书中的最后一章,我们也不会遗憾地来回顾其形成过程。

本节讨论了 Go 语言的并发功能的“坑”。

8.1. 保持自己忙碌或做自己的工作

这个程序有什么问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}()

for {
}
}

该程序实现了我们的预期,它提供简单的 Web 服务。 然而,它同时也做了其他事情,它在无限循环中浪费 CPU 资源。 这是因为 main 的最后一行上的 for {} 将阻塞 main goroutine,因为它不执行任何 IO、等待锁定、发送或接收通道数据或以其他方式与调度器通信。

由于 Go 语言运行时主要是协同调度,该程序将在单个 CPU 上做无效地旋转,并可能最终实时锁定。

我们如何解决这个问题? 这是一个建议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"log"
"net/http"
"runtime"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}()

for {
runtime.Gosched()
}
}

这看起来很愚蠢,但这是我看过的一种常见解决方案。 这是不了解潜在问题的症状。

现在,如果你有更多的经验,你可能会写这样的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
go func() {
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}()

select {}
}

空的 select 语句将永远阻塞。 这是一个有用的属性,因为现在我们不再调用 runtime.GoSched() 而耗费整个 CPU。 但是这也只是治疗了症状,而不是病根。

我想向你提出另一种你可能在用的解决方案。 与其在 goroutine 中运行 http.ListenAndServe,会给我们留下处理 main goroutine 的问题,不如在 main goroutine 本身上运行 http.ListenAndServe

贴士:
如果 Go 语言程序的 main.main 函数返回,无论程序在一段时间内启动的其他 goroutine 在做什么, Go 语言程序会无条件地退出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"log"
"net/http"
)

func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, GopherCon SG")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

所以这是我的第一条建议:如果你的 goroutine 在得到另一个结果之前无法取得进展,那么让自己完成此工作而不是委托给其他 goroutine 会更简单。

这通常会消除将结果从 goroutine 返回到其启动程序所需的大量状态跟踪和通道操作。

贴士:
许多 Go 程序员过度使用 goroutine,特别是刚开始时。与生活中的所有事情一样,适度是成功的关键。

8.2. 将并发性留给调用者

以下两个 API 有什么区别?

1
2
// ListDirectory returns the contents of dir.
func ListDirectory(dir string) ([]string, error)
1
2
3
4
// ListDirectory returns a channel over which
// directory entries will be published. When the list
// of entries is exhausted, the channel will be closed.
func ListDirectory(dir string) chan string

首先,最明显的不同: 第一个示例将目录读入切片然后返回整个切片,如果出错则返回错误。这是同步发生的,ListDirectory 的调用者会阻塞,直到读取了所有目录条目。根据目录的大小,这可能需要很长时间,并且可能会分配大量内存来构建目录条目。

让我们看看第二个例子。 这个示例更像是 Go 语言风格,ListDirectory 返回一个通道,通过该通道传递目录条目。当通道关闭时,表明没有更多目录条目。由于在 ListDirectory 返回后发生了通道的填充,ListDirectory 可能会启动一个 goroutine 来填充通道。

注意:
第二个版本实际上不必使用 Go 协程; 它可以分配一个足以保存所有目录条目而不阻塞的通道,填充通道,关闭它,然后将通道返回给调用者。但这样做不太现实,因为会消耗大量内存来缓冲通道中的所有结果。

通道版本的 ListDirectory 还有两个问题:

  • 通过使用关闭通道作为没有其他项目要处理的信号,在中途遇到了错误时, ListDirectory 无法告诉调用者通过通道返回的项目集是否完整。调用者无法区分空目录和读取目录的错误。两者都导致从 ListDirectory 返回的通道立即关闭。
  • 调用者必须持续从通道中读取,直到它被关闭,因为这是调用者知道此通道的是否停止的唯一方式。这是对 ListDirectory 使用的严重限制,即使可能已经收到了它想要的答案,调用者也必须花时间从通道中读取。就中型到大型目录的内存使用而言,它可能更有效,但这种方法并不比原始的基于切片的方法快。

以上两种实现所带来的问题的解决方案是使用回调,该回调是在执行时在每个目录条目的上下文中调用函数。

1
func ListDirectory(dir string, fn func(string))

毫不奇怪,这就是 filepath.WalkDir 函数的工作方式。

贴士:
如果你的函数启动了 goroutine,你必须为调用者提供一种明确停止 goroutine 的方法。 把异步执行函数的决定留给该函数的调用者通常会更容易些。

8.3. 永远不要启动一个停止不了的 goroutine。

前面的例子显示当一个任务时没有必要时使用 goroutine。但使用 Go 语言的原因之一是该语言提供的并发功能。实际上,很多情况下你希望利用硬件中可用的并行性。为此,你必须使用 goroutines

这个简单的应用程序在两个不同的端口上提供 http 服务,端口 8080 用于应用程序服务,端口 8001 用于访问 /debug/pprof 终端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"net/http"
_ "net/http/pprof"
)

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
go http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux) // debug
http.ListenAndServe("0.0.0.0:8080", mux) // app traffic
}

虽然这个程序不是很复杂,但它代表了真实应用程序的基础。

该应用程序存在一些问题,因为它随着应用程序的增长而显露出来,所以我们现在来解决其中的一些问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
http.ListenAndServe("0.0.0.0:8080", mux)
}

func serveDebug() {
http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}

func main() {
go serveDebug()
serveApp()
}

通过将 serveAppserveDebug 处理程序分解成为它们自己的函数,我们将它们与 main.main 分离。 也遵循了上面的建议,并确保 serveAppserveDebug 将它们的并发性留给调用者。

但是这个程序存在一些可操作性问题。 如果 serveApp 返回,那么 main.main 将返回,导致程序关闭并由你使用的进程管理器来重新启动。

贴士:
正如 Go 语言中的函数将并发性留给调用者一样,应用程序应该将监视其状态和检测是否重启的工作留给另外的程序来做。 不要让你的应用程序负责重新启动自己,最好从应用程序外部处理该过程。

然而,serveDebug 是在一个单独的 goroutine 中运行的,返回后该 goroutine 将退出,而程序的其余部分继续。 由于 /debug 处理程序已停止工作很久,因此操作人员不会很高兴发现他们无法在你的应用程序中获取统计信息。

我们想要确保的是,如果任何负责提供此应用程序的 goroutine 停止,我们将关闭该应用程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func serveApp() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
if err := http.ListenAndServe("0.0.0.0:8080", mux); err != nil {
log.Fatal(err)
}
}

func serveDebug() {
if err := http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux); err != nil {
log.Fatal(err)
}
}

func main() {
go serveDebug()
go serveApp()
select {}
}

现在 serverAppserveDebug 检查从 ListenAndServe 返回的错误,并在需要时调用 log.Fatal。因为两个处理程序都在 goroutine 中运行,所以我们将 main goroutine 停在 select{} 中。

这种方法存在许多问题:

  1. 如果 ListenAndServer 返回 nil 错误,则不会调用 log.Fatal,并且该端口上的 HTTP 服务将在不停止应用程序的情况下关闭。
  2. log.Fatal 调用 os.Exit,它将无条件地退出程序; defer 不会被调用,其他 goroutines 也不会被通知关闭,程序就停止了。 这使得编写这些函数的测试变得困难。

贴士:
只在 main.maininit 函数中的使用 log.Fatal

我们真正想要的是任何错误发送回 goroutine 的调用者,以便它可以知道 goroutine 停止的原因,可以干净地关闭程序进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func serveApp() error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
return http.ListenAndServe("0.0.0.0:8080", mux)
}

func serveDebug() error {
return http.ListenAndServe("127.0.0.1:8001", http.DefaultServeMux)
}

func main() {
done := make(chan error, 2)
go func() {
done <- serveDebug()
}()
go func() {
done <- serveApp()
}()

for i := 0; i < cap(done); i++ {
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
}
}

我们可以使用通道来收集 goroutine 的返回状态。通道的大小等于我们想要管理的 goroutine 的数量,这样发送到 done 通道就不会阻塞,因为这会阻止 goroutine 的关闭,导致它泄漏。

由于没有办法安全地关闭 done 通道,我们不能使用 for range 来循环通道直到获取所有 goroutine 发来的报告,而是循环我们开启的多个 goroutine,即通道的容量。

现在我们有办法等待每个 goroutine 干净地退出并记录他们遇到的错误。所需要的只是一种从第一个 goroutine 转发关闭信号到其他 goroutine 的方法。

事实证明,要求 http.Server 关闭是有点牵扯的,所以我将这个逻辑转给辅助函数。serve 助手使用一个地址和 http.Handler,类似于 http.ListenAndServe,还有一个 stop 通道,我们用它来触发 Shutdown 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
func serve(addr string, handler http.Handler, stop <-chan struct{}) error {
s := http.Server{
Addr: addr,
Handler: handler,
}

go func() {
<-stop // wait for stop signal
s.Shutdown(context.Background())
}()

return s.ListenAndServe()
}

func serveApp(stop <-chan struct{}) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(resp http.ResponseWriter, req *http.Request) {
fmt.Fprintln(resp, "Hello, QCon!")
})
return serve("0.0.0.0:8080", mux, stop)
}

func serveDebug(stop <-chan struct{}) error {
return serve("127.0.0.1:8001", http.DefaultServeMux, stop)
}

func main() {
done := make(chan error, 2)
stop := make(chan struct{})
go func() {
done <- serveDebug(stop)
}()
go func() {
done <- serveApp(stop)
}()

var stopped bool
for i := 0; i < cap(done); i++ {
if err := <-done; err != nil {
fmt.Println("error: %v", err)
}
if !stopped {
stopped = true
close(stop)
}
}
}

现在,每次我们在 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

10. https://blog.golang.org/errors-are-values

11. http://www.gopl.io/


原文链接:Practical Go: Real world advice for writing maintainable Go programs

  • 如有翻译有误或者不理解的地方,请评论指正
  • 待更新的译注之后会做进一步修改翻译
  • 翻译:田浩
  • 邮箱:llitfkitfk@gmail.com

HexoClient1.2.9版本发布

本次更新内容

  • 支持草稿功能
  • 支持检查更新功能
  • 修复创建文章时ctrl+s多次保存会生成多篇文章的问题
  • 修复选中分类、标签展示之后从其他页面切换回来选中状态丢失的问题

功能预览

QQ20190124111534.png

相关链接

协同过滤和基于内容推荐有什么区别?

根据数据源的不同推荐引擎可以分为三类

1、基于人口的统计学推荐(Demographic-based Recommendation)

2、基于内容的推荐(Content-based Recommendation)

3、基于协同过滤的推荐(Collaborative Filtering-based Recommendation)

基于内容的推荐:

根据物品或内容的元数据,发现物品或内容的相关性,然后基于用户以前的喜好记录推荐给用户相似的物品,如图所示:
image.png
上图给出了基于内容推荐的一个典型的例子,电影推荐系统,首先我们需要对电影的元数据有一个建模,这里只简单的描述了一下电影的类型;然后通过电影的元数据发现电影间的相似度,因为类型都是“爱情,浪漫”电影 A 和 C 被认为是相似的电影(当然,只根据类型是不够的,要得到更好的推荐,我们还可以考虑电影的导演,演员等等);最后实现推荐,对于用户 A,他喜欢看电影 A,那么系统就可以给他推荐类似的电影 C。

而基于协同过滤推荐又分为以下三类:

(1)基于用户的协同过滤推荐(User-based Collaborative Filtering Recommendation)

基于用户的协同过滤推荐算法先使用统计技术寻找与目标用户有相同喜好的邻居,然后根据目标用户的邻居的喜好产生向目标用户的推荐。基本原理就是利用用户访问行为的相似性来互相推荐用户可能感兴趣的资源,如图所示:
image.png
上图示意出基于用户的协同过滤推荐机制的基本原理,假设用户 A 喜欢物品 A、物品 C,用户 B 喜欢物品 B,用户 C 喜欢物品 A 、物品 C 和物品 D;从这些用户的历史喜好信息中,我们可以发现用户 A 和用户 C 的口味和偏好是比较类似的,同时用户 C 还喜欢物品 D,那么我们可以推断用户 A 可能也喜欢物品 D,因此可以将物品 D 推荐给用户 A。

(2)基于项目的协同过滤推荐(Item-based Collaborative Filtering Recommendation)

根据所有用户对物品或者信息的评价,发现物品和物品之间的相似度,然后根据用户的历史偏好信息将类似的物品推荐给该用户,如图所示:
image.png
上图表明基于项目的协同过滤推荐的基本原理,用户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 部分: 推荐引擎初探,然后结合其他资料理解总结的,如有更好意见谢谢分享。

HexoClient 1.2.8版本发布

本次更新内容

功能预览

QQ20190124111534.png

相关链接

Golang并发编程

Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。

Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。

下面我们来看一个例子,在线演示:https://play.golang.org/p/U9U-qjuY0t1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个goroutine
go runing()
// 创建一个匿名的goroutine
go func() {
fmt.Println("喜特:" + time.Now().String())
}()

// 这里sleep一下是因为main方法如果执行完了,main该程序创建的所有goroutine都会退出
time.Sleep(5 * time.Second)
}

func runing() {
fmt.Println("法克:" + time.Now().String())
time.Sleep(3 * time.Second)
}

输出:
法克:2009-11-10 23:00:00 +0000 UTC m=+0.000000001
喜特:2009-11-10 23:00:00 +0000 UTC m=+0.000000001

执行结果说明fuck函数中的sleep三秒并没有影响喜特的输出。

如果说 goroutine 是Go语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。

下面我们利用goroutine+channel来实现一个生产消费者模型,示例代码如下,在线执行:https://play.golang.org/p/lqUBugLdU-I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个通道
channel := make(chan int64)

// 异步去生产
go producer(channel)

// 数据消费
consumer(channel)
}

// 生产者
func producer(channel chan<- int64) {
for {
// 将数据写入通道
channel <- time.Now().Unix()
// 睡1秒钟
time.Sleep(time.Second)
}
}

// 消费者
func consumer(channel <-chan int64) {
for {
timestamp := <-channel
fmt.Println(timestamp)
}
}

输出为如下:(每秒钟打印一次)
1257894000
1257894001
1257894002
1257894003

Golang指针类型和值类型

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
a := "123"
b := &a

fmt.Println(a)
fmt.Println(b)
fmt.Println(*b)
}

输出结果为:
123
0x40c128
123

Java程序员Go语言入门简介

为什么是Go语言

  • 类C的语法,这意味着Java、C#、JavaScript程序员能很快的上手
  • 有自己的垃圾回收机制
  • 跨平台、编译即可执行无需安装依赖环境
  • 支持反射

Go语言简介

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

基本语法

HelloWorld

在线运行示例:https://play.golang.org/p/-4RylAqUV36

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

var name string

func init() {
name = "world"
}

func main() {
fmt.Println("hello " + name)
}

我们来执行一下:

1
2
$ go run main.go # main.go 为刚刚创建的那个文件的名称
$ hello world

变量

变量声明

在线运行示例:https://play.golang.org/p/zPqCkRZgrgp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
)

func main() {
var name string // 声明
name = "gaoyoubo" // 赋值
fmt.Println(name)

var age int = 18 // 声明并赋值
fmt.Println(age)
}

类型推断

在线运行示例:https://play.golang.org/p/0My8veBvtJ8

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
)

func main() {
name := "gaoyoubo"
fmt.Println(name)

age := 18
fmt.Println(age)
}

函数

  • 函数可以有多个返回值
  • 隐式的指定函数是private还是public,函数首字母大写的为public、小写的为private
  • 没有类似Java中的try cachethrow,Go语言是通过将error作为返回值来处理异常。
  • 不支持重载

下面我们通过一个示例来了解一下,在线运行示例:https://play.golang.org/p/PYy3ueuPFS6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"errors"
"fmt"
"strconv"
)

func main() {
log1()

log2("hello world")

ret1 := add1(1, 1)
fmt.Println("add1 result:" + strconv.Itoa(ret1))

ret2, err := Add2(0, 1)
if err == nil {
fmt.Println("Add2 result:" + strconv.Itoa(ret2))
} else {
fmt.Println("Add2 error", err)
}
}

// 私有、无入参、无返回值
func log1() {
fmt.Println("execute func log1")
}

// 私有、入参、无返回值
func log2(msg string) {
fmt.Println("execute func log2:" + msg)
}

// 私有、两个入参、一个返回值
func add1(count1, count2 int) int {
total := count1 + count2
fmt.Println("execute func add3, result=" + strconv.Itoa(total))
return total
}

// Public、两个入参、多个返回值
func Add2(count1, count2 int) (int, error) {
if count1 < 1 || count2 < 1 {
return 0, errors.New("数量不能小于1")
}
total := count1 + count2
return total, nil
}

该示例输出结果为:

1
2
3
4
5
execute func log1
execute func log2:hello world
execute func add3, result=2
add1 result:2
Add2 error 数量不能小于1

但函数有多个返回值的时候,有时你只关注其中一个返回值,这种情况下你可以将其他的返回值赋值给空白符:_,如下:

1
2
3
4
_, err := Add2(1, 2)
if err != nil {
fmt.Println(err)
}

空白符特殊在于实际上返回值并没有赋值,所以你可以随意将不同类型的值赋值给他,而不会由于类型不同而报错。

结构体

Go语言不是像Java那样的面向对象的语言,他没有对象和继承的概念。也没有class的概念。在Go语言中有个概念叫做结构体(struct),结构体和Java中的class比较类似。下面我们定义一个结构体:

1
2
3
4
5
type User struct {
Name string
Gender string
Age int
}

上面我们定义了一个结构体User,并为该结构体分别设置了三个公有属性:Name/Gender/Age,下面我们来创建一个User对象。

1
2
3
4
5
user := User{
Name: "hahaha",
Gender: "男",
Age: 18, // 值得一提的是,最后的逗号是必须的,否则编译器会报错,这就是go的设计哲学之一,要求强一致性。
}

结构体的属性可以在结构体内直接声明,那么如何为结构体声明函数(即Java中的方法)呢,我们来看下下面的示例:在线运行示例:https://play.golang.org/p/01_cTu0RzdH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type User struct {
Name string
Gender string
Age int
}

// 定义User的成员方法
func (u *User) addAge() {
u.Age = u.Age + 1
}

func main() {
user := User{
Name: "哈", // 名称
Gender: "男", // 性别
Age: 18, // 值得一提的是,最后的逗号是必须的,否则编译器会报错,这就是go的设计哲学之一,要求强一致性。
}
user.addAge()
fmt.Println(user.Age)
}

指针类型和值类型

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
)

func main() {
a := "123"
b := &a

fmt.Println(a)
fmt.Println(b)
fmt.Println(*b)
}

输出结果为:
123
0x40c128
123

并发编程

Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。

Go 程序从 main 包的 main() 函数开始,在程序启动时,Go 程序就会为 main() 函数创建一个默认的 goroutine。

下面我们来看一个例子(在线演示:https://play.golang.org/p/U9U-qjuY0t1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个goroutine
go runing()
// 创建一个匿名的goroutine
go func() {
fmt.Println("喜特:" + time.Now().String())
}()

// 这里sleep一下是因为main方法如果执行完了,main该程序创建的所有goroutine都会退出
time.Sleep(5 * time.Second)
}

func runing() {
fmt.Println("法克:" + time.Now().String())
time.Sleep(3 * time.Second)
}

输出:
法克:2009-11-10 23:00:00 +0000 UTC m=+0.000000001
喜特:2009-11-10 23:00:00 +0000 UTC m=+0.000000001

执行结果说明fuck函数中的sleep三秒并没有影响喜特的输出。

如果说 goroutine 是Go语言程序的并发体的话,那么 channel 就是它们之间的通信机制。一个 channel 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channel 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。

下面我们利用goroutine+channel来实现一个生产消费者模型,示例代码如下:(在线执行:https://play.golang.org/p/lqUBugLdU-I)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"time"
)

func main() {
// 创建一个通道
channel := make(chan int64)

// 异步去生产
go producer(channel)

// 数据消费
consumer(channel)
}

// 生产者
func producer(channel chan<- int64) {
for {
// 将数据写入通道
channel <- time.Now().Unix()
// 睡1秒钟
time.Sleep(time.Second)
}
}

// 消费者
func consumer(channel <-chan int64) {
for {
timestamp := <-channel
fmt.Println(timestamp)
}
}

输出为如下:(每秒钟打印一次)
1257894000
1257894001
1257894002
1257894003

Java程序员觉得不好用的地方

  • 异常处理
  • 没有泛型
  • 不支持多态、重载
  • 不支持注解(但是他的struct中的属性支持tag

参考

Git删除远程tag

删除本地tag很简单,直接:git tag -d tagname 但是我们这样删除之后远程标签其实并未删除,通过以下方式可以删除远程标签。

1
2
3
4
5
6
7
8
# 假设标签名称为:v1.0.0

# 删除本地标签
git tag -d v1.0.0

# 删除远程标签
git push origin :refs/tags/v1.0.0

多个Github账号时,怎么配置sshkey

我有两个Github账号,在配置sshkey的时候是会提示Key is already in use。因为github无法将相同的sshkey配置到不同的账号下,那么就要考虑同一台机器如何配置两个sshkey了。

生成第二个sshkey

因为之前已经存在~/.ssh/id_rsa文件,所以这次生成的时候我们指定输出的文件名为id_rsa2

1
ssh-keygen -t rsa -C "example@example.com" -f ~/.ssh/id_rsa2

创建config配置文件

~/.ssh/目录下创建config配置文件,内容如下,里面有详细的解释。

1
2
3
4
5
6
7
8
9
10
11
# 原有的配置
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa

# 第二个配置
Host github_2.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa2 # 这里指定下所使用的公钥文件名,就是我们上一步新生成的那个。

使用第二个sshkey配置

假如我们的仓库地址为: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

搭建Shadowsocks-Server

之前一直使用的搬瓦工的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
2
3
4
5
6
7
8
9
{
"server":"127.0.0.1",
"server_port":8388,
"local_port":1080,
"local_address":"127.0.0.1",
"password":"helloworld",
"method": "aes-128-cfb",
"timeout":600
}

另外阿里云默认情况下8388端口是不对外开放的,请去阿里云控制到,找到对应的ecs实例,找到该实例的安全组,将8388端口添加到白名单。

修改mariadb的datadir

目标:将mariadb默认的datadir(/var/lib/mysql)迁移到/data/mysql

停止mariadb服务

1
systemctl stop mariadb

创建新datadir

1
2
mkdir /data/mysql
chown -R mysql:mysql /data/mysql

将数据文件复制过来

1
cp -a /var/lib/mysql    /data/mysql

修改配置

1
2
3
4
5
6
vi /etc/my.cnf

[mysqld]
# datadir=/var/lib/mysql
# 注释掉之前的,将datadir设置成新目录
datadir=/data/mysql

重新启动mariadb服务

1
systemctl start mariadb

Centos7初始化安装mariadb

安装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
2
3
Set root password? [Y/n] <– 是否设置root用户密码,输入y并回车或直接回车
New password: <– 设置root用户的密码
Re-enter new password: <– 再输入一次你设置的密码

其他配置

1
2
3
4
5
6
7
Remove anonymous users? [Y/n] <– 是否删除匿名用户,回车

Disallow root login remotely? [Y/n] <–是否禁止root远程登录,回车,

Remove test database and access to it? [Y/n] <– 是否删除test数据库,回车

Reload privilege tables now? [Y/n] <– 是否重新加载权限表,回车

初始化MariaDB完成,接下来测试登录

1
mysql -uroot -ppassword

完成。

JavaScript计算农历

产品最近需要花一个万年历的图片用于分享使用,找了Java的很多库都不好用,于是在其他万年历的网页中找到了下面的代码,然后用Java调用JavaScript将万年历计算出来。

最终生成的图片

image.png

JavaScript代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
<!DOCTYPE html>
<html>
<head>
<title>Calendar</title>
<script type="text/javascript">
var getData = (function () {
//公历农历转换
var calendar = {
lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2,
0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977,
0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970,
0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950,
0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557,
0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0,
0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0,
0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6,
0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570,
0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x055c0, 0x0ab60, 0x096d5, 0x092e0,
0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5,
0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930,
0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530,
0x05aa0, 0x076a3, 0x096d0, 0x04bd7, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45,
0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0,
0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0,
0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4,
0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0,
0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160,
0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252,
0x0d520],
solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
Gan: ["\u7532", "\u4e59", "\u4e19", "\u4e01", "\u620a", "\u5df1", "\u5e9a", "\u8f9b", "\u58ec", "\u7678"],
Zhi: ["\u5b50", "\u4e11", "\u5bc5", "\u536f", "\u8fb0", "\u5df3", "\u5348", "\u672a", "\u7533", "\u9149", "\u620c", "\u4ea5"],
Animals: ["\u9f20", "\u725b", "\u864e", "\u5154", "\u9f99", "\u86c7", "\u9a6c", "\u7f8a", "\u7334", "\u9e21", "\u72d7", "\u732a"],
solarTerm: ["\u5c0f\u5bd2", "\u5927\u5bd2", "\u7acb\u6625", "\u96e8\u6c34", "\u60ca\u86f0", "\u6625\u5206", "\u6e05\u660e", "\u8c37\u96e8", "\u7acb\u590f", "\u5c0f\u6ee1", "\u8292\u79cd", "\u590f\u81f3", "\u5c0f\u6691", "\u5927\u6691", "\u7acb\u79cb", "\u5904\u6691", "\u767d\u9732", "\u79cb\u5206", "\u5bd2\u9732", "\u971c\u964d", "\u7acb\u51ac", "\u5c0f\u96ea", "\u5927\u96ea", "\u51ac\u81f3"],
sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f',
'97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f',
'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa',
'97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f',
'97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f',
'97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722',
'9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f',
'97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722',
'7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e',
'97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e',
'97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2',
'9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa',
'97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2',
'977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722',
'7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722',
'977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722',
'977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722',
'7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721',
'7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd',
'7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35',
'7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722',
'7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721',
'7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5',
'7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35',
'665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721',
'7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd',
'7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35',
'7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'],
nStr1: ["\u65e5", "\u4e00", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341"],
nStr2: ["\u521d", "\u5341", "\u5eff", "\u5345"],
nStr3: ["\u6b63", "\u4e8c", "\u4e09", "\u56db", "\u4e94", "\u516d", "\u4e03", "\u516b", "\u4e5d", "\u5341", "\u51ac", "\u814a"],
lYearDays: function (y) {
var i, sum = 348;
for (i = 0x8000; i > 0x8; i >>= 1) { sum += (calendar.lunarInfo[y - 1900] & i) ? 1 : 0; }
return (sum + calendar.leapDays(y));
},
leapMonth: function (y) {
return (calendar.lunarInfo[y - 1900] & 0xf);
},
leapDays: function (y) {
if (calendar.leapMonth(y)) {
return ((calendar.lunarInfo[y - 1900] & 0x10000) ? 30 : 29);
}
return (0);
},
monthDays: function (y, m) {
if (m > 12 || m < 1) { return -1 }
return ((calendar.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29);
},
solarDays: function (y, m) {
if (m > 12 || m < 1) { return -1 }
var ms = m - 1;
if (ms == 1) {
return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28);
} else {
return (calendar.solarMonth[ms]);
}
},
toGanZhi: function (offset) {
return (calendar.Gan[offset % 10] + calendar.Zhi[offset % 12]);
},
getTerm: function (y, n) {
if (y < 1900 || y > 2100) { return -1; }
if (n < 1 || n > 24) { return -1; }
var _table = calendar.sTermInfo[y - 1900];
var _info = [
parseInt('0x' + _table.substr(0, 5)).toString(),
parseInt('0x' + _table.substr(5, 5)).toString(),
parseInt('0x' + _table.substr(10, 5)).toString(),
parseInt('0x' + _table.substr(15, 5)).toString(),
parseInt('0x' + _table.substr(20, 5)).toString(),
parseInt('0x' + _table.substr(25, 5)).toString()
];
var _calday = [
_info[0].substr(0, 1),
_info[0].substr(1, 2),
_info[0].substr(3, 1),
_info[0].substr(4, 2),
_info[1].substr(0, 1),
_info[1].substr(1, 2),
_info[1].substr(3, 1),
_info[1].substr(4, 2),
_info[2].substr(0, 1),
_info[2].substr(1, 2),
_info[2].substr(3, 1),
_info[2].substr(4, 2),
_info[3].substr(0, 1),
_info[3].substr(1, 2),
_info[3].substr(3, 1),
_info[3].substr(4, 2),
_info[4].substr(0, 1),
_info[4].substr(1, 2),
_info[4].substr(3, 1),
_info[4].substr(4, 2),
_info[5].substr(0, 1),
_info[5].substr(1, 2),
_info[5].substr(3, 1),
_info[5].substr(4, 2),
];
return parseInt(_calday[n - 1]);
},
toChinaMonth: function (m) {
if (m > 12 || m < 1) { return -1 }
var s = calendar.nStr3[m - 1];
s += "\u6708";
return s;
},
toChinaDay: function (d) {
var s;
switch (d) {
case 10:
s = '\u521d\u5341';
break;
case 20:
s = '\u4e8c\u5341';
break;
case 30:
s = '\u4e09\u5341';
break;
default:
s = calendar.nStr2[Math.floor(d / 10)];
s += calendar.nStr1[d % 10];
}
return (s);
},
getAnimal: function (y) {
return calendar.Animals[(y - 4) % 12]
},
solar2lunar: function (y, m, d) {
if (y < 1900 || y > 2100) { return -1; }
if (y == 1900 && m == 1 && d < 31) { return -1; }
if (!y) {
var objDate = new Date();
} else {
var objDate = new Date(y, parseInt(m) - 1, d)
}
var i, leap = 0, temp = 0;
var y = objDate.getFullYear(), m = objDate.getMonth() + 1, d = objDate.getDate();
var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000;
for (i = 1900; i < 2101 && offset > 0; i++) { temp = calendar.lYearDays(i); offset -= temp; }
if (offset < 0) { offset += temp; i--; }
var isTodayObj = new Date(), isToday = false;
if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) {
isToday = true;
}
var nWeek = objDate.getDay(), cWeek = calendar.nStr1[nWeek];
if (nWeek == 0) { nWeek = 7; }
var year = i;
var leap = calendar.leapMonth(i);
var isLeap = false;
for (i = 1; i < 13 && offset > 0; i++) {
if (leap > 0 && i == (leap + 1) && isLeap == false) {
--i;
isLeap = true; temp = calendar.leapDays(year);
} else {
temp = calendar.monthDays(year, i);
}
if (isLeap == true && i == (leap + 1)) { isLeap = false; }
offset -= temp;
}
if (offset == 0 && leap > 0 && i == leap + 1) {
if (isLeap) {
isLeap = false;
} else {
isLeap = true; --i;
}
}
if (offset < 0) { offset += temp; --i; }
var month = i;
var day = offset + 1;
var sm = m - 1;
var term3 = calendar.getTerm(year, 3);
var gzY = calendar.toGanZhi(year - 4);
gzY = calendar.toGanZhi(year - 4); //modify
var firstNode = calendar.getTerm(y, (m * 2 - 1));
var secondNode = calendar.getTerm(y, (m * 2));
var gzM = calendar.toGanZhi((y - 1900) * 12 + m + 11);
if (d >= firstNode) {
gzM = calendar.toGanZhi((y - 1900) * 12 + m + 12);
}
var isTerm = false;
var Term = null;
if (firstNode == d) {
isTerm = true;
Term = calendar.solarTerm[m * 2 - 2];
}
if (secondNode == d) {
isTerm = true;
Term = calendar.solarTerm[m * 2 - 1];
}
var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10;
var gzD = calendar.toGanZhi(dayCyclical + d - 1);
return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': calendar.getAnimal(year), 'IMonthCn': (isLeap ? "\u95f0" : '') + calendar.toChinaMonth(month), 'IDayCn': calendar.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': "\u661f\u671f" + cWeek, 'isTerm': isTerm, 'Term': Term };
}
};
//公历节日
var _festival1 = {
'0101': '元旦节',
'0202': '世界湿地日',
'0210': '国际气象节',
'0214': '情人节',
'0301': '国际海豹日',
'0303': '全国爱耳日',
'0305': '学雷锋纪念日',
'0308': '妇女节',
'0312': '植树节',
'0314': '国际警察日',
'0315': '消费者权益日',
'0317': '中国国医节 国际航海日',
'0321': '世界森林日 消除种族歧视国际日 世界儿歌日',
'0322': '世界水日',
'0323': '世界气象日',
'0324': '世界防治结核病日',
'0325': '全国中小学生安全教育日',
'0401': '愚人节',
'0407': '世界卫生日',
'0422': '世界地球日',
'0423': '世界图书和版权日',
'0424': '亚非新闻工作者日',
'0501': '劳动节',
'0504': '青年节',
'0515': '防治碘缺乏病日',
'0508': '世界红十字日',
'0512': '国际护士节',
'0515': '国际家庭日',
'0517': '世界电信日',
'0518': '国际博物馆日',
'0520': '全国学生营养日',
'0522': '国际生物多样性日',
'0531': '世界无烟日',
'0601': '国际儿童节 世界牛奶日',
'0605': '世界环境日',
'0606': '全国爱眼日',
'0617': '防治荒漠化和干旱日',
'0623': '国际奥林匹克日',
'0625': '全国土地日',
'0626': '国际禁毒日',
'0701': '建党节 香港回归纪念日',
'0702': '国际体育记者日',
'0711': '世界人口日 航海日',
'0801': '建军节',
'0808': '中国男子节(爸爸节)',
'0903': '抗日战争胜利纪念日',
'0908': '国际扫盲日 国际新闻工作者日',
'0910': '教师节',
'0916': '国际臭氧层保护日',
'0918': '九·一八事变纪念日',
'0920': '国际爱牙日',
'0927': '世界旅游日',
'1001': '国庆节 国际音乐日 国际老人节',
'1002': '国际非暴力日 国际和平与民主自由斗争日',
'1004': '世界动物日',
'1006': '老人节',
'1008': '全国高血压日',
'1005': '国际教师节',
'1009': '世界邮政日',
'1010': '辛亥革命纪念日 世界精神卫生日',
'1013': '世界保健日 国际减灾日',
'1014': '世界标准日',
'1015': '国际盲人节(白手杖节)',
'1016': '世界粮食日',
'1017': '世界消除贫困日',
'1022': '世界传统医药日',
'1024': '联合国日 世界发展信息日',
'1031': '世界勤俭日',
'1107': '十月社会主义革命纪念日',
'1108': '中国记者日',
'1109': '全国消防安全宣传教育日',
'1110': '世界青年节',
'1111': '国际科学与和平周(本日所属的一周)',
'1112': '孙中山诞辰纪念日',
'1114': '联合国糖尿病日',
'1117': '国际大学生节',
'1121': '世界问候日 世界电视日',
'1129': '国际声援巴勒斯坦人民国际日',
'1201': '世界艾滋病日',
'1203': '世界残疾人日',
'1204': '宪法日',
'1205': '国际志愿人员日',
'1209': '世界足球日',
'1210': '世界人权日',
'1212': '西安事变纪念日',
'1213': '南京大屠杀纪念日',
'1220': '澳门回归纪念',
'1221': '国际篮球日',
'1224': '平安夜',
'1225': '圣诞节',
'1226': '毛泽东诞辰纪念日'
};
//某月的第几个星期几,第3位为5表示最后一星期
var _festival2 = {
'0110': '黑人日',
'0150': '世界麻风日',
'0440': '世界儿童日',
'0520': '国际母亲节',
'0532': '国际牛奶日',
'0530': '全国助残日',
'0630': '父亲节',
'0711': '世界建筑日',
'0730': '被奴役国家周',
'0936': '世界清洁地球日',
'0932': '国际和平日',
'0940': '国际聋人节',
'1011': '国际住房日',
'1024': '世界视觉日',
'1144': '感恩节',
'1220': '国际儿童电视广播日'
};
//农历节日
var _festival3 = {
'0101': '春节',
'0102': '初二',
'0103': '初三',
'0115': '元宵节',
'0202': '龙抬头节',
'0323': '妈祖生辰',
'0505': '端午节',
'0707': '七夕节',
'0715': '中元节',
'0815': '中秋节',
'0909': '重阳节',
'1208': '腊八节',
'1223': '小年',
'0100': '除夕'
};
//假日安排数据
var _holiday = {
'2011': { '0402': 0, '0403': 1, '0404': 1, '0405': 1, '0430': 1, '0501': 1, '0502': 1, '0604': 1, '0605': 1, '0606': 1, '0910': 1, '0911': 1, '0912': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 0, '1009': 0, '1231': 0 },
'2012': {
'0101': 1, '0102': 1, '0103': 1, '0121': 0, '0122': 1, '0123': 1, '0124': 1, '0125': 1, '0126': 1, '0127': 1, '0128': 1, '0129': 0, '0331': 0, '0401'
: 0, '0402': 1, '0403': 1, '0404': 1, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0622': 1, '0623': 1, '0624': 1, '0929': 0, '0930': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1
},
'2013': { '0101': 1, '0102': 1, '0103': 1, '0105': 0, '0106': 0, '0209': 1, '0210': 1, '0211': 1, '0212': 1, '0213': 1, '0214': 1, '0215': 1, '0216': 0, '0217': 0, '0404': 1, '0405': 1, '0406': 1, '0407': 0, '0427': 0, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0608': 0, '0609': 0, '0610': 1, '0611': 1, '0612': 1, '0919': 1, '0920': 1, '0921': 1, '0922': 0, '0929': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1012': 0 },
'2014': { '0101': 1, '0126': 0, '0131': 1, '0201': 1, '0202': 1, '0203': 1, '0203': 1, '0204': 1, '0205': 1, '0206': 1, '0208': 0, '0405': 1, '0406': 1, '0407': 1, '0501': 1, '0502': 1, '0503': 1, '0504': 0, '0531': 1, '0601': 1, '0602': 1, '0908': 1, '0928': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1011': 0 },
'2015': { '0101': 1, '0102': 1, '0103': 1, '0104': 0, '0215': 0, '0218': 1, '0219': 1, '0220': 1, '0221': 1, '0222': 1, '0223': 1, '0224': 1, '0228': 0, '0404': 1, '0405': 1, '0406': 1, '0501': 1, '0502': 1, '0503': 1, '0620': 1, '0621': 1, '0622': 1, '0903': 1, '0904': 1, '0905': 1, '0906': 0, '0927': 1, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1010': 0 },
'2016': { '0101': 1, '0102': 1, '0103': 1, '0206': 0, '0207': 1, '0208': 1, '0209': 1, '0210': 1, '0211': 1, '0212': 1, '0213': 1, '0214': 0, '0402': 1, '0403': 1, '0404': 1, '0430': 1, '0501': 1, '0502': 1, '0609': 1, '0610': 1, '0611': 1, '0612': 0, '0915': 1, '0916': 1, '0917': 1, '0918': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 0, '1009': 0 },
'2017': { '0101': 1, '0102': 1, '0122': 0, '0127': 1, '0128': 1, '0129': 1, '0130': 1, '0131': 1, '0201': 1, '0202': 1, '0204': 0, '0401': 0, '0402': 1, '0403': 1, '0404': 1, '0429': 1, '0430': 1, '0501': 1, '0527': 0, '0528': 1, '0529': 1, '0530': 1, '0930': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1008': 1, '1230': 1, '1231': 1 },
'2018': { '0101': 1, '0211': 0, '0215': 1, '0216': 1, '0217': 1, '0218': 1, '0219': 1, '0220': 1, '0221': 1, '0224': 0, '0405': 1, '0406': 1, '0407': 1, '0408': 0, '0428': 0, '0429': 1, '0430': 1, '0501': 1, '0616': 1, '0617': 1, '0618': 1, '0922': 1, '0923': 1, '0924': 1, '0929': 0, '0930': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1229': 0, '1230': 1, '1231': 1 },
'2019': { '0101': 1, '0202': 0, '0203': 0, '0204': 1, '0205': 1, '0206': 1, '0207': 1, '0208': 1, '0209': 1, '0210': 1, '0405': 1, '0406': 1, '0407': 1, '0428': 0, '0501': 1, '0502': 1, '0503': 1, '0504': 1, '0505': 0, '0607': 1, '0608': 1, '0609': 1, '0913': 1, '0914': 1, '0915': 1, '0929': 0, '1001': 1, '1002': 1, '1003': 1, '1004': 1, '1005': 1, '1006': 1, '1007': 1, '1012': 0 }
};
//获取日期数据
var getDateObj = function (year, month, day) {
var date = arguments.length && year ? new Date(year, month - 1, day) : new Date();
return {
'year': date.getFullYear(),
'month': date.getMonth() + 1,
'day': date.getDate(),
'week': date.getDay()
};
};
//当天
var _today = getDateObj();
//获取当月天数
var getMonthDays = function (obj) {
var day = new Date(obj.year, obj.month, 0);
return day.getDate();
};
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
}
//获取某天日期信息
var getDateInfo = function (obj) {
var info = calendar.solar2lunar(obj.year, obj.month, obj.day);
var cMonth = info.cMonth > 9 ? '' + info.cMonth : '0' + info.cMonth;
var cDay = info.cDay > 9 ? '' + info.cDay : '0' + info.cDay;
var lMonth = info.lMonth > 9 ? '' + info.lMonth : '0' + info.lMonth;
var lDay = info.lDay > 9 ? '' + info.lDay : '0' + info.lDay;
var code1 = cMonth + cDay;
var code2 = cMonth + Math.ceil(info.cDay / 7) + info.nWeek % 7;
var code3 = lMonth + lDay;
var days = getMonthDays(obj);
//节日信息
info['festival'] = '';
if (_festival3[code3]) {
info['festival'] += _festival3[code3];
}
if (_festival1[code1]) {
info['festival'] += ' ' + _festival1[code1];
}
if (_festival2[code2]) {
info['festival'] += ' ' + _festival2[code2];
}
if (obj['day'] + 7 > days) {
var code4 = cMonth + 5 + info.nWeek % 7;
if (code4 != code2 && _festival2[code4]) {
info['festival'] += ' ' + _festival2[code4];
}
}
info['festival'] = info['festival'].trim();
//放假、调休等标记
info['sign'] = '';
if (_holiday[info.cYear]) {
var holiday = _holiday[info.cYear];
if (typeof holiday[code1] != 'undefined') {
info['sign'] = holiday[code1] ? 'holiday' : 'work';
}
}
if (info.cYear == _today.year && info.cMonth == _today.month && info.cDay == _today.day) {
info['sign'] = 'today';
}
return info;
};
//获取日历信息
return (function (date) {
var date = date || _today;
var first = getDateObj(date['year'], date['month'], 1); //当月第一天
var days = getMonthDays(date); //当月天数
var data = []; //日历信息
var obj = {};
//上月日期
for (var i = first['week']; i > 0; i--) {
obj = getDateObj(first['year'], first['month'], first['day'] - i);
var info = getDateInfo(obj);
info['disabled'] = 1;
data.push(info);
}
//当月日期
for (var i = 0; i < days; i++) {
obj = {
'year': first['year'],
'month': first['month'],
'day': first['day'] + i,
'week': (first['week'] + i) % 7
};
var info = getDateInfo(obj);
info['disabled'] = 0;
data.push(info);
}
//下月日期
var last = obj;
for (var i = 1; last['week'] + i < 7; i++) {
obj = getDateObj(last['year'], last['month'], last['day'] + i);
var info = getDateInfo(obj);
info['disabled'] = 1;
data.push(info);
}
return {
'date': getDateInfo(date), //当前日历选中日期
'data': data
};
});
})();

function test() {
console.log(getData({ 'year': 2019, 'month': 5, 'day': 14 }))
}
</script>
</head>
<body>
<button onclick="test()">日历信息</button>
</body>
</html>

Markdown 语法指南

Markdown 语法指南

语法详解

粗体

1
2
**粗体**
__粗体__

斜体

1
2
*斜体*
_斜体_

标题

1
2
3
4
5
6
7
8
9
10
# 一级标题 #
一级标题
====
## 二级标题 ##
二级标题
----
### 三级标题 ###
#### 四级标题 ####
##### 五级标题 #####
###### 六级标题 ######

分割线

1
2
***
---

^上^角

1
2
上角标 x^2^
下角标 H~2~0

++下划线++ 中划线

1
2
++下划线++
~~中划线~~

==标记==

1
==标记==

段落引用

1
2
3
4
> 一级
>> 二级
>>> 三级
...

列表

1
2
3
4
5
6
7
8
9
有序列表
1.
2.
3.
...
无序列表
-
-
...

任务列表

  • 已完成任务
  • 未完成任务
1
2
- [x] 已完成任务
- [ ] 未完成任务

链接

1
2
[链接](www.baidu.com)
![图片描述](http://www.image.com)

代码段落

``` type

代码段落

```

` 代码块 `

1
2
3
4
int main()
{
printf("hello world!");
}

code

表格(table)

1
2
3
4
| 标题1 | 标题2 | 标题3 |
| :-- | :--: | ----: |
| 左对齐 | 居中 | 右对齐 |
| ---------------------- | ------------- | ----------------- |
标题1 标题2 标题3
左对齐 居中 右对齐
———————- ————- —————–

脚注(footnote)

1
hello[^hello]

见底部脚注^hello

表情(emoji)

参考网站: https://www.webpagefx.com/tools/emoji-cheat-sheet/

1
2
3
4
5
:laughing:
:blush:
:smiley:
:)
...

:laughing::blush::smiley::)

$\KaTeX$公式

我们可以渲染公式例如:$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
2
3
4
5
6
7
8
9
10
11
12
术语一

: 定义一

包含有*行内标记*的术语二

: 定义二

{一些定义二的文字或代码}

定义二的第三段

abbr

*[HTML]: Hyper Text Markup Language
*[W3C]: World Wide Web Consortium
HTML 规范由 W3C 维护

1
2
3
*[HTML]: Hyper Text Markup Language
*[W3C]: World Wide Web Consortium
HTML 规范由 W3C 维护

Go 简易教程

关于本书

授权许可

本书中的内容使用 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 被设计成代码在工作区内运行。工作区是一个文件夹,这个文件夹由 binpkg,以及src子文件夹组成的。你可能会试图强迫 Go 遵循自己的风格-不要这么去做。

一般,我把我的项目放在 ~/code 文件夹下。比如,~/code/blog 目录就包含了我的 blog 项目。对于 Go 来说,我的工作区域就是 ~/code/go ,然后我的 Go 写的项目代码就在 ~/code/go/src/blog 文件夹下。

简单来说,无论你希望把你的项目放在哪里,你最好创建一个 go 的文件夹,再在里面创建一个 src 的子文件夹。

OSX / Linux 系统安装 Go

下载适合你自己电脑系统的 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 就安装完成了。

Windows 系统

下载最新的 zip 文件。如果你的电脑是 64 位的系统,你将需要 go#.#.#.windows-amd64.zip ,这里的 #.#.# 是 Go 的最新版本号。

解压缩 go#.#.#.windows-amd64.zip 文件到你选择的位置。 c:\Go这个位置是个不错的选择。

在系统中设置两个环境变量:

  1. GOPATH 同样的指向的是你的工作目录。这个变量看起来像c:\users\goku\work\go 这个样子。
  2. 添加 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,C++,Java,JavaScript 和 C#,你会觉得 Go 的语法很熟悉——最少表面上是这样的。举个例子,&& 用于逻辑 AND,== 用于判断是否相等,{} 是块的开始和结束,数组下标的起始值为 0。

类 C 语法也倾向于用分号表示作为语句结束符,并将条件写在括号中。Go 不支持这些,但是仍然使用括号来控制优先级。例如,一个 if 语句是这样的:

1
2
3
if name == "Leto" {
print("the spice must flow")
}

在很多复杂系统中,括号符还是很有用的:

1
2
3
if (name == "Goku" && power > 9000) || (name == "gohan" && power < 4000)  {
print("super Saiyan")
}

除此之外,Go 要比 C# 或 Java 更接近 C - 不仅是语法方面,还有目的方面。这反映在语言的简洁和简单上,希望你在学习它的时候能慢慢体会这一点。

变量和申明

如果我们用 x = 4 来申明和赋值变量,那么我们就可以同时开始和结束对变量的查看了。遗憾的是,Go 更为复杂些。我们将通过简单的示例来开始我们的学习。然后,在下一章中,我们会在创建和使用结构体时,进一步扩展。尽管如此,你可能还得花一段时间来适应,才能感受到它带给你的舒适感。

你可能会想:“哇!这有什么复杂的?”。 让我们开始一个例子。

下面的例子是 Go 中,申明变量和赋值最为明确的方法,但也是最为冗长的方法:

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)

func main() {
var power int
power = 9000
fmt.Printf("It's over %d\n", power)
}

这里我们定义了一个 int 类型的变量 power。默认情况下,Go 会为变量分配默认值。Integers 的默认值是 0,booleans 默认值是 false,strings 默认值是 "" 等等。下面,我们创建一个值为 9000 的名为 power 的变量。我们可以将定义和赋值两行代码合并起来:

1
var power int = 9000

不过,这么写太长了。Go 提供了一个方便的短变量声明运算符 := ,它可以自动推断变量类型:

1
power := 9000

这非常方便,它可以跟函数结合使用,就像这样:

1
2
3
4
5
6
7
func main() {
power := getPower()
}

func getPower() int {
return 9001
}

值得注意的是要用 := 来声明变量以及给变量赋值。相同变量不能被声明两次(在相同作用域下),如果你尝试这样,会收到错误提示。

1
2
3
4
5
6
7
8
9
func main() {
power := 9000
fmt.Printf("It's over %d\n", power)

// 编译器错误:
// := 左侧不是新的变量
power := 9001
fmt.Printf("It's also over %d\n", power)
}

编辑器会告诉你 * := 左侧不是新的变量*。这就意味着当我们首次声明一个变量时应该使用 := ,后面再给变量赋值时应该使用 =。这似乎很有道理,但是凭空来记忆且需要根据情况来切换却是很难的事。

如果你仔细阅读代码的错误信息,你会发现 variables 单词是个复数,即有多个变量,那是因为go支持多个变量同时赋值(使用 = 或者 :=):

1
2
3
4
func main() {
name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}

另外,多个变量赋值的时候,只要其中有一个变量是新的,就可以使用:=。例如:

1
2
3
4
5
6
7
func main() {
power := 1000
fmt.Printf("default power is %d\n", power)

name, power := "Goku", 9000
fmt.Printf("%s's power is over %d\n", name, power)
}

尽管变量 power 使用了两次:=,但是编译器不会在第 2 次使用 :=时报错,因为这里有一个新的 name变量,它可以使用:=。然后你不能改变 power 变量的类型,它已经被声明成一个整型,所以只能赋值整数。

到目前为止,你最后需要了解的一件事是,Go 会像 import 一样,不允许你在程序中拥有未使用的变量。例如:

1
2
3
4
func main() {
name, power := "Goku", 1000
fmt.Printf("default power is %d\n", power)
}

这将不会通过编译,因为 name 是一个被申明但是未被使用的变量,就像 import 的包未被使用时,也将会导致编译失败,但总的来说,我认为这有助于提高代码的清洁度和可读性。

还有更多关于的申明和赋值的技巧。初始化一个变量时,请使用: var NAME TYPE;给变量申明及赋值时,请使用: NAME := VALUE ; 给之前已经申明过的变量赋值时,请使用: NAME = VALUE

垃圾回收

一些变量,在创建的时候,就拥有一个简单定义的生命周期。对于函数中的变量,会在函数执行完后进行销毁。在别的语言中,对于编译器而言,这不会很明显。例如:函数返回的变量,或者由其他变量和对象所调用的变量,它们的生命周期是很难确定的。 如果没有垃圾回收机制,那么开发人员就得知道有哪些不需要用到的变量,并将它们释放。就像 C 语言,你需要使用 free(str); 来释放变量。

语言的垃圾回收机制(像:Ruby, Python, Java, JavaScript, C# , Go)是会对变量进行跟踪,并在没有使用它们的时候,进行释放。垃圾回收会增加一些额外的开销,但是也减少了一些致命性的 BUG。

运行 go 代码

创建一个简单的程序然后学习如何编译和运行它。打开你的文本编辑器写入下面的代码:

1
2
3
4
5
package main

func main() {
println("it's over 9000!")
}

保存文件并命名为 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 产生的二进制文件并且执行它。

入口函数 Main

希望刚才执行的代码是可以理解的。我们刚刚创建了一个函数,并且使用内置函数 println 打印出了字符串。难道仅因为这里只有一个选择,所以 go run 知道执行什么吗??不。在 go 中程序入口必须是 main 函数,并且在 main 包内。

我们将在后面的章节中详细介绍。目前,我们将专注于理解 go 基础,一直会在 main 包中写代码。

如果你想尝试,你可以修改代码并且可以更改包名。使用 go run 执行程序将出现一个错误。 接着你可以将包名改回 main ,换一个不同的方法名,你会看到一个不同的错误。尝试使用 go build 代替 go run 来执行刚才的代码,注意代码编译时,没有入口点可以执行。但当你构建一个库时,确实完全正确的。

导入包

Go 有很多内建函数,例如 println,可以在没有引用情况下直接使用。但是,如果不使用 Go 的标准库直接使用第三方库,我们就无法走的更远。import 关键字被用于去声明文件中代码要使用的包。

修改下我们的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"os"
)

func main() {
if len(os.Args) != 2 {
os.Exit(1)
}
fmt.Println("It's over", os.Args[1])
}

你可以这样运行:

1
go run main.go 9000

我们现在用了 Go 的两个标准包:fmtos 。我们也介绍了另一个内建函数 lenlen 返回字符串的长度,字典值的数量,或者我们这里看到的,它返回了数组元素的数量。如果你想知道我们这里为什么期望得到两个参数,它是因为第一个参数 – 索引0处 – 总是当前可运行程序的路径。(更改程序将它打印出来亲自看看就知道了)

你可能注意到了,我们在函数名称前加上了前缀包名,例如,fmt.PrintLn。这是不同于其他很多语言的。后续的章节中我们将学习到更多关于包的知识。现在,知道如何导入以及使用一个包就是一个好的开始。

在 Go 中,关于导包是很严格的。如果你导入了一个包却没有使用将会导致编译不通过。尝试运行下面的程序:

1
2
3
4
5
6
7
8
9
package main

import (
"fmt"
"os"
)

func main() {
}

你应该会得到两个关于 fmtos 被导入却没有被使用的错误。这很烦人的是不是呀?绝对是这样的,不过随着时间的推移,你将慢慢习惯它(虽然仍然烦人,不过要以 Go 的思维写 Go)。Go 如此严格是因为没用的导入会降低编译速度;诚然,我们大多数人不会关注这个问题。

另一个需要记住的事情是 Go 的标准库已经有了很好的文档。你可以访问 https://golang.org/pkg/fmt/#Println 去看更多关于 PrintLn 函数的信息。你可以点击那个部分的头去看源代码。另外,也可以滚动到顶部查看关于 Go 格式化功能的更多消息。

如果没有互联网,你可以这样在本地获取文档:

1
godoc -http=:6060

然后浏览器中访问 http://localhost:6060

函数声明

这是个好时机指出函数是可以返回多个值的。让我们看三个函数:一个没有返回值,一个有一个返回值,一个有两个返回值。

1
2
3
4
5
6
7
8
func log(message string) {
}

func add(a int, b int) int {
}

func power(name string) (int, bool) {
}

我们可以像这样使用最后一个:

1
2
3
4
value, exists := power("goku")
if exists == false {
// 处理错误情况
}

有时候,你仅仅关注其中一个返回值。这个情况下,你可以将其他的返回值赋值给空白符_

1
2
3
4
_, exists := power("goku")
if exists == false {
// handle this error case
}

这不仅仅是一个惯例。_ ,空白标识符,特殊在于实际上返回值并没有赋值。这让你可以一遍又一遍地使用 _ 而不用管它的类型。

最后,关于函数声明还有些要说的。如果参数有相同的类型,您可以用这样一个简洁的用法:

1
2
3
func add(a, b int) int {

}

返回多个值可能是你经常使用的,你也可能会频繁地使用 _ 丢弃一个值。命名返回值和稍微冗长的参数声明不太常用。尽管如此,你将很快遇到他们,所以了解他们很重要。

继续之前

我们之前看了很多小的独立片段,在这点上,可能会感到有点脱节。我们将慢慢地构建更大的例子,将这些小的片段组合在一起。

如果你之前用的是动态类型语言,那么类型和声明的复杂性看起来像是在倒退。我并没有不同意你,在某些系统中动态语言可能更加有效。

如果你来自静态类型的语言,你可能对 Go 感到满意。类型推断以及多值返回的设计非常棒?(尽管这不是 Go 独有)。希望随着我们了解更多,你将会慢慢爱上这干净简洁的语法。

第二章 · 结构体

Go 不是像 C ++,Java,Ruby和C#一样的面向对象的(OO)语言。它没有对象和继承的概念,也没有很多与面向对象相关的概念,例如多态和重载。

Go所具有的是结构体的概念,可以将一些方法和结构体关联。Go 还支持一种简单但有效的组合形式。 总的来说,它会使代码变的更简单,但在某一些场合,你会错过面向对象提供的一些特性。(值得指出的是,通过组合实现继承是一场古老的战斗呐喊,Go 是我用过的第一种坚定立场的语言,在这个问题上。)

虽然 Go 不会像你以前使用的面向对象语言一样,但是你会注意到结构的定义和类的定义之间有很多相似之处。下面的代码定义了一个简单的 Saiyan 结构体:

1
2
3
4
type Saiyan struct {
Name string
Power int
}

我们将看明白怎么往这个结构体添加一个方法,就像面向对象类,会有方法作为 它的一部分。在这之前,我们先要知道如何申明结构体。

声明和初始化

当我们第一次看到变量和声明时,我们只看了内置类型,比如整数和字符串。既然现在我们要讨论结构,那么我们需要扩展讨论范围到指针。

创建结构的值的最简单的方式是:

1
2
3
4
goku := Saiyan{
Name: "Goku",
Power: 9000,
}

注意: 上述结构末尾的逗号 , 是必需的。没有它的话,编译器就会报错。你将会喜欢上这种必需的一致性,尤其当你使用一个与这种风格相反的语言或格式的时候。

我们不必设置所有或哪怕一个字段。下面这些都是有效的:

1
2
3
4
5
6
goku := Saiyan{}

// or

goku := Saiyan{Name: "Goku"}
goku.Power = 9000

就像未赋值的变量其值默认为 0 一样,字段也是如此。

此外,你可以不写字段名,依赖字段顺序去初始化结构体 (但是为了可读性,你应该把字段名写清楚):

1
goku := Saiyan{"Goku", 9000}

以上所有的示例都是声明变量 goku 并赋值。

许多时候,我们并不想让一个变量直接关联到值,而是让它的值为一个指针,通过指针关联到值。一个指针就是内存中的一个地址;指针的值就是实际值的地址。这是间接地获取值的方式。形象地来说,指针和实际值的关系就相当于房子和指向该房子的方向之间的关系。

为什么我们想要一个指针指向值而不是直接包含该值呢?这归结为 Go 中传递参数到函数的方式:就像复制。知道了这个,尝试理解一下下面的代码呢?

1
2
3
4
5
6
7
8
9
func main() {
goku := Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}

func Super(s Saiyan) {
s.Power += 10000
}

上面程序运行的结果是 9000,而不是 19000,。为什么?因为 Super 修改了原始值 goku 的复制版本,而不是它本身,所以,Super 中的修改并不影响上层调用者。现在为了达到你的期望,我们可以传递一个指针到函数中:

1
2
3
4
5
6
7
8
9
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
s.Power += 10000
}

这一次,我们修改了两处代码。第一个是使用了 & 操作符以获取值的地址(它就是 取地址 操作符)。然后,我们修改了 Super 参数期望的类型。它之前期望一个 Saiyan 类型,但是现在期望一个地址类型 *Saiyan,这里 *X 意思是 指向类型 X 值的指针 。很显然类型 Saiyan*Saiyan 是有关系的,但是他们是不同的类型。

这里注意到我们仍然传递了一个 goku 的值的副本给 Super,但这时 goku 的值其实是一个地址。所以这个副本值也是一个与原值相等的地址,这就是我们间接传值的方式。想象一下,就像复制一个指向饭店的方向牌。你所拥有的是一个方向牌的副本,但是它仍然指向原来的饭店。

我们可以证实一下这是一个地址的副本,通过修改其指向的值(尽管这可能不是你真正想做的事情):

1
2
3
4
5
6
7
8
9
func main() {
goku := &Saiyan{"Goku", 9000}
Super(goku)
fmt.Println(goku.Power)
}

func Super(s *Saiyan) {
s = &Saiyan{"Gohan", 1000}
}

上面的代码,又一次地输出 9000。就像许多语言表现的那样,包括 Ruby,Python, Java 和 C#,Go 以及部分的 C#,只是让这个事实变得更明显一些。

同样很明显的是,复制一个指针比复制一个复杂的结构的消耗小多了。在 64 位的机器上面,一个指针占据 64 bit 的空间。如果我们有一个包含很多字段的结构,创建它的副本将会是一个很昂贵的操作。指针的真正价值在于能够分享它所指向的值。我们是想让 Super 修改 goku 的副本还是修改共享的 goku 值本身呢?

所有这些并不是说你总应该使用指针。这章末尾,在我们见识了结构的更多功能以后,我们将重新检视 指针与值这个问题。

结构体上的函数

我们可以把一个方法关联在一个结构体上:

1
2
3
4
5
6
7
8
type Saiyan struct {
Name string
Power int
}

func (s *Saiyan) Super() {
s.Power += 10000
}

在上面的代码中,我们可以这么理解,*Saiyan 类型是 Super 方法的接受者。然后我们可以通过下面的代码去调用 Super 方法:

1
2
3
goku := &Saiyan{"Goku", 9001}
goku.Super()
fmt.Println(goku.Power) // 将会打印出 19001

构造器

结构体没有构造器。但是,你可以创建一个返回所期望类型的实例的函数(类似于工厂):

1
2
3
4
5
6
func NewSaiyan(name string, power int) *Saiyan {
return &Saiyan{
Name: name,
Power: power,
}
}

这种模式以错误的方式惹恼了很多开发人员。一方面,这里有一点轻微的语法变化;另一方面,它确实感觉有点不那么明显。

我们的工厂不必返回一个指针;下面的形式是完全有效的:

1
2
3
4
5
6
func NewSaiyan(name string, power int) Saiyan {
return Saiyan{
Name: name,
Power: power,
}
}

结构体的字段

到目前为止的例子中,Saiyan 有两个字段 NamePower,其类型分别为 stringint。字段可以是任何类型 – 包括其他结构体类型以及目前我们还没有提及的 array,maps,interfaces 和 functions 等类型。

例如,我们可以扩展 Saiyan 的定义:

1
2
3
4
5
type Saiyan struct {
Name string
Power int
Father *Saiyan
}

然后我们通过下面的方式初始化:

1
2
3
4
5
6
7
8
9
gohan := &Saiyan{
Name: "Gohan",
Power: 1000,
Father: &Saiyan {
Name: "Goku",
Power: 9001,
Father: nil,
},
}

New

尽管缺少构造器,Go 语言却有一个内置的 new 函数,使用它来分配类型所需要的内存。 new(X) 的结果与 &X{} 相同。

1
2
3
goku := new(Saiyan)
// same as
goku := &Saiyan{}

如何使用取决于你,但是你会发现大多数人更偏爱后一种写法无论是否有字段需要初始化,因为这看起来更具可读性:

1
2
3
4
5
6
7
8
9
10
goku := new(Saiyan)
goku.name = "goku"
goku.power = 9001

//vs

goku := &Saiyan {
Name: "goku",
Power: 9000,
}

无论你选择哪一种,如果你遵循上述的工厂模式,就可以保护剩余的代码而不必知道或担心内存分配细节

组合

Go 支持组合, 这是将一个结构包含进另一个结构的行为。在某些语言中,这种行为叫做 特质 或者 混合。 没有明确的组合机制的语言总是可以做到这一点。在 Java 中, 可以使用 继承 来扩展结构。但是在脚本中并没有这种选项, 混合将会被写成如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Person {
private String name;

public String getName() {
return this.name;
}
}

public class Saiyan {
// Saiyan 中包含着 person 对象
private Person person;

// 将请求转发到 person 中
public String getName() {
return this.person.getName();
}
...
}

这可能会非常繁琐。Person 的每个方法都需要在 Saiyan 中重复。Go 避免了这种复杂性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Person struct {
Name string
}

func (p *Person) Introduce() {
fmt.Printf("Hi, I'm %s\n", p.Name)
}

type Saiyan struct {
*Person
Power int
}

// 使用它
goku := &Saiyan{
Person: &Person{"Goku"},
Power: 9001,
}
goku.Introduce()

Saiyan 结构体有一个 Person 类型的字段。由于我们没有显示地给它一个字段名,所以我们可以隐式地访问组合类型的字段和函数。然而,Go 编译器确实给了它一个字段,下面这样完全有效:

1
2
3
4
5
goku := &Saiyan{
Person: &Person{"Goku"},
}
fmt.Println(goku.Name)
fmt.Println(goku.Person.Name)

上面两个都打印 「Goku」。

组合比继承更好吗?许多人认为它是一种更好的组织代码的方式。当使用继承的时候,你的类和超类紧密耦合在一起,你最终专注于结构而不是行为。

指针类型和值类型

当你写 Go 代码的时候,很自然就会去问自己 应该是值还是指向值的指针呢? 这儿有两个好消息,首先,无论我们讨论下面哪一项,答案都是一样的:

  • 局部变量赋值
  • 结构体指针
  • 函数返回值
  • 函数参数
  • 方法接收器

第二,如果你不确定,那就用指针咯。

正如我们已经看到的,传值是一个使数据不可变的好方法(函数中改变它不会反映到调用代码中)。有时,这是你想要的行为,但是通常情况下,不是这样的。

即使你不打算改变数据,也要考虑创建大型结构体副本的成本。相反,你可能有一些小的结构:

1
2
3
4
type Point struct {
X int
Y int
}

这种情况下,复制结构的成本能够通过直接访问 XY 来抵消,而没有其它任何间接操作。

还有,这些案例都是很微妙的,除非你迭代成千上万个这样的指针,否则你不会注意到差异。

继续之前

从实际的角度看,这章介绍了结构体,如何使一个结构体的实例成为函数的接收者,以及添加指针到现有的 Go 类型系统知识中。下面的章节将建立在我们已经了解了什么是结构体以及其内部工作原理之上。

第三章 · 映射、数组和切片

至此,我们已经学了一部分简单的类型和结构。现在,让我们开始学习 Arrays (数组), Slices (切片) 和 Maps (映射) 吧。

数组

如果你学过 Python , Ruby , Perl , JavaScript 或者 PHP (或者更多其它的语言),那么你肯定习惯 动态数组 编程啦。这些数组的长度可以在添加数据的时候自行调整的。在 Go 中,像其它大部分语言一样,数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的了:

1
2
var scores [10]int
scores[0] = 339

上面的数组最多可以容纳 10 个元素,索引是从 scores[0]scores[9] 。试图访问超过界限的索引系统将会抛出编译或运行时错误。

我们可以在初始化数组的时候指定值:

1
scores := [4]int{9001, 9333, 212, 33}

我们可以使用 len 函数来获取数组的长度。range 函数在遍历迭代的时候使用:

1
2
3
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 的切片。(如果你仔细观察的话,你会注意到 makelen 重载了。Go 的一些特性没有暴露出来给开发者使用,这也许会让你感到沮丧。)

为了更好的理解切片的长度和容量之间的关系,我们来看下面的的例子:

1
2
3
4
5
func main() {
scores := make([]int, 0, 10)
scores[7] = 9033
fmt.Println(scores)
}

我们上面的这个例子不能运行,为什么呢?因为切片的长度是 0 。没错,底层数组可以放 10 个元素,但是我们需要显式的扩展切片,才能访问到底层数组的元素。一种扩展切片的方式是通过 append的关键字来实现:

1
2
3
4
5
func main() {
scores := make([]int, 0, 10)
scores = append(scores, 5)
fmt.Println(scores) // prints [5]
}

但是那并没有改变原始代码的意图。追加一个值到长度为0的切片中将会设置第一个元素。无论什么原因,我们崩溃的代码想去设置索引为7的元素值。为了实现这个,我们可以重新切片:

1
2
3
4
5
6
func main() {
scores := make([]int, 0, 10)
scores = scores[0:8]
scores[7] = 9033
fmt.Println(scores)
}

我们可以调整的切片大小最大范围是多少呢?达到它的容量,这个例子中,是10。你可能在想 这实际上并没有解决数组固定长度的问题。但是 append 是相当特别的。如果底层数组满了,它将创建一个更大的数组并且复制所有原切片中的值(这个就很像动态语言 PHP,Python,Ruby,JavaScript 的工作方式)。这就是为什么上面的例子中我们必须重新将 append 返回的值赋值给 scores 变量:append 可能在原有底层数组空间不足的情况下创建了新值。

如果我告诉你 Go 使用 2x 算法来增加数组长度,你猜下面将会打印什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
scores := make([]int, 0, 5)
c := cap(scores)
fmt.Println(c)

for i := 0; i < 25; i++ {
scores = append(scores, i)

// 如果容量改变了
// Go 必须增加数组长度来容纳新的数据
if cap(scores) != c {
c = cap(scores)
fmt.Println(c)
}
}
}

初始 scores 的容量是5。为了存储25个值,它必须扩展三次容量,分别是 10,20,最终是40。

最后一个例子,考虑这个:

1
2
3
4
5
func main() {
scores := make([]int, 5)
scores = append(scores, 9332)
fmt.Println(scores)
}

这里输出是 [0, 0, 0, 0, 0, 9332],可能你觉得是[9332, 0, 0, 0, 0]?对一个用户而言,这可能逻辑上是正确的。然而,对于一个编译器,你告诉他的是追加一个值到一个已经有5个值的切片。

最终,这有四种方式初始化一个切片:

1
2
3
4
names := []string{"leto", "jessica", "paul"}
checks := make([]bool, 10)
var names []string
scores := make([]int, 0, 20)

什么时候该用哪个呢?第一个不用过多解释。当你事先知道数组中的值的时候,你可以使用这个方式。

当你想要写入切片具体的索引时,第二个方法很有用,例如:

1
2
3
4
5
6
7
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, len(saiyans))
for index, saiyan := range saiyans {
powers[index] = saiyan.Power
}
return powers
}

第三个版本是指向空的切片,用于当元素数量未知时与 append 连接。

最后一个版本是让我们声明一个初始的容量。如果我们大概知道元素的数量将是很有用的。

即使当你知道大小的时候,append 也可以使用,取决于个人偏好:

1
2
3
4
5
6
7
func extractPowers(saiyans []*Saiyans) []int {
powers := make([]int, 0, len(saiyans))
for _, saiyan := range saiyans {
powers = append(powers, saiyan.Power)
}
return powers
}

切片作为数组的包装是一个很强大的概念。许多语言有切片数组的概念。JavaScript 和 Ruby 数组都有一个 slice 方法。Ruby 中你可以使用 [START..END] 获取一个切片,或者 Python 中可以通过 [START:END] 实现。然而,在这些语言中,一个切片实际上是复制了原始值的新数组。如果我们使用 Ruby,下面这段代码的输出是什么呢?

1
2
3
4
scores = [1,2,3,4,5]
slice = scores[2..4]
slice[0] = 999
puts scores

答案是 [1, 2, 3, 4, 5] 。那是因为 slice 是一个新数组,并且复制了原有的值。现在,考虑 Go 中的情况:

1
2
3
4
scores := []int{1,2,3,4,5}
slice := scores[2:4]
slice[0] = 999
fmt.Println(scores)

输出是 [1, 2, 999, 4, 5]

这改变了你编码的方式。例如,许多函数采用一个位置参数。JavaScript 中,如果你想去找到字符串中前五个字符后面的第一个空格(当然,在Go中切片也可以用于字符串),我们会这样写:

1
2
haystack = "the spice must flow";
console.log(haystack.indexOf(" ", 5));

在 Go 中,我们这样使用切片:

1
strings.Index(haystack[5:], " ")

我们可以从上面的例子中看到,[X:]从 X 到结尾 的简写,然而 [:X]从开始到 X 的简写。不像其他的语言,Go 不支持负数索引。如果我们想要切片中除了最后一个元素的所有值,可以这样写:

1
2
scores := []int{1, 2, 3, 4, 5}
scores = scores[:len(scores)-1]

上面是从未排序的切片中移除元素的有效方法的开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
scores := []int{1, 2, 3, 4, 5}
scores = removeAtIndex(scores, 2)
fmt.Println(scores) // [1 2 5 4]
}

// 不会保持顺序
func removeAtIndex(source []int, index int) []int {
lastIndex := len(source) - 1
// 交换最后一个值和想去移除的值
source[index], source[lastIndex] = source[lastIndex], source[index]
return source[:lastIndex]
}

最后,我们已经了解了切片,我们再看另一个通用的内建函数:copy。正常情况下,将值从一个数组复制到另一个数组的方法有5个参数,sourcesourceStartcount,,destinationdestinationStart。使用切片,我们仅仅需要两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (
"fmt"
"math/rand"
"sort"
)

func main() {
scores := make([]int, 100)
for i := 0; i < 100; i++ {
scores[i] = int(rand.Int31n(1000))
}
sort.Ints(scores)

worst := make([]int, 5)
copy(worst, scores[:5])
fmt.Println(worst)
}

花点时间试试上面的代码,并尝试改动。去看看如果你这么做 copy(worst[2:4], scores[:5]) 或者复制多于或少于 5 个值给 worst 会发什么?

映射

Go语言中的映射,就好比其他语言中的hash表或者字典。它们的工作方式就是:定义键和值,并且可以获取,设置和删除其中的值。

映射和切片一样,使用 make 方法来创建。让我们来看看一个例子:

1
2
3
4
5
6
7
8
9
func main() {
lookup := make(map[string]int)
lookup["goku"] = 9001
power, exists := lookup["vegeta"]

// prints 0, false
// 0 is the default value for an integer
fmt.Println(power, exists)
}

我们使用 len方法类获取映射的键的数量。使用delete方法来删除一个键对应的值:

1
2
3
4
5
// returns 1
total := len(lookup)

// has no return, can be called on a non-existing key
delete(lookup, "goku")

映射是动态变化的。然而我们可以通过传递第二个参数到 make方法来设置一个初始大小:

1
lookup := make(map[string]int, 100)

如果你事先知道映射会有多少键值,定义一个初始大小将会帮助改善性能。

当你需要将映射作为结构体字段的时候,你可以这样定义它:

1
2
3
4
type Saiyan struct {
Name string
Friends map[string]*Saiyan
}

初始上述结构体的一种方式是:

1
2
3
4
5
goku := &Saiyan{
Name: "Goku",
Friends: make(map[string]*Saiyan),
}
goku.Friends["krillin"] = ... //加载或者创建 Krillin

Go 还有一种定义和初始化值的方式。像 make,这种特定用于映射和数组。我们可以定义为复合方式:

1
2
3
4
lookup := map[string]int{
"goku": 9001,
"gohan": 2044,
}

我们可以使用 for 组合 range 关键字迭代映射:

1
2
3
for key, value := range lookup {
...
}

迭代映射是没有顺序的。每次迭代查找将会随机返回键值对。

指针和值

第二章我们讨论了到底是传值还是传指针。现在我们有相同的问题在映射和数组上,到底该使用他们哪个?

1
2
3
a := make([]Saiyan, 10)
// 或者
b := 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
2
3
4
5
6
7
8
9
10
11
package db

type Item struct {
Price float64
}

func LoadItem(id int) *Item {
return &Item{
Price: 9.001,
}
}

需要注意包名和文件夹名是一样的。而且很明显我们实际并没有连接数据库。这里使用这个例子只是为了展示如何组织代码。

现在,创建在主目录 shopping 下创建一个叫 pricecheck.go 的文件。它的内容是:

1
2
3
4
5
6
7
8
9
10
11
12
13
package shopping

import (
"shopping/db"
)

func PriceCheck(itemId int) (float64, bool) {
item := db.LoadItem(itemId)
if item == nil {
return 0, false
}
return item.Price, true
}

很有可能认为导入 shopping/db 有点特别,因为我们已经在 shopping 包/目录中。实际上,我们正在导入 $GOPATH/src/shopping/db,这意味着只要你在你的工作区间 src/test 目录中有一个名为 db 的包,你就可以轻松导入它。

你正在构建一个包,除了我们看到的你不再需要任何东西。为了构建一个可执行程序,你仍然需要 main 包。我比较喜欢的方式是在 shopping 目录下创建一个 main 子目录,然后再创建一个叫 main.go 的文件,下面是它的内容:

1
2
3
4
5
6
7
8
9
10
package main

import (
"shopping"
"fmt"
)

func main() {
fmt.Println(shopping.PriceCheck(4343))
}

现在,你可以进入你的 shopping 项目运行代码,输入:

1
go run main/main.go

循环导入

当你编写更复杂的系统的时,你必然会遇到循环导入。例如,当 A 包导入 B 包,B 包又导入 A 包(间接或者直接导入)。这是编译器不能允许的。

让我们改变我们的 shopping 结构以复现这个错误。

Item 定义从 shopping/db/db.go 移到 shopping/pricecheck.go。你的 pricecheck.go 文件像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package shopping

import (
"shopping/db"
)

type Item struct {
Price float64
}

func PriceCheck(itemId int) (float64, bool) {
item := db.LoadItem(itemId)
if item == nil {
return 0, false
}
return item.Price, true
}

如果你尝试运行代码,你会从 db/db.go 得到两个关于 Item 未定义的错误。这看起来是说 Item 不存在 db 包中。它已经被移动到 shopping 包中,我们需要将 shopping/db/db.go 改变成:

1
2
3
4
5
6
7
8
9
10
11
package db

import (
"shopping"
)

func LoadItem(id int) *shopping.Item {
return &shopping.Item{
Price: 9.001,
}
}

现在但你尝试运行代码的时候,你将会得到 不允许循环导入 的错误。我们可以通过引入另一个包含共享结构体的包来解决这个问题。你的目录现在看起来像这个样子:

1
2
3
4
5
6
7
8
9
$GOPATH/src
- shopping
pricecheck.go
- db
db.go
- models
item.go
- main
main.go

pricecheck.go 将仍然导入 shopping/db,但是 db.go 现在导入 shopping/models 而不是 shopping,因此打破了循环。因为我们将共享的 Item 结构体移动到 shopping/models/item.go,我们现在需要去改变 shopping/db/db.gomodels 包中引用 Item 结构体。

1
2
3
4
5
6
7
8
9
10
11
package db

import (
"shopping/models"
)

func LoadItem(id int) *models.Item {
return &models.Item{
Price: 9.001,
}
}

你经常需要共享某些代码,不止 models,所以你可能有其他类似叫做 utilities 的目录,这些共享包的重要原则是他们不从 shopping 包或者任何子包中导入任何东西。在后面的章节中,我们将介绍可以帮助我们解决这些类型依赖关系的接口。

可见性

Go 用了一个简单的规则去定义什么类型和函数可以包外可见。如果类型或者函数名称以一个大写字母开始,它就具有了包外可见性。如果以一个小写字母开始,它就不可以。

这也可以应用到结构体字段。如果一个字段名以一个小写字母开始,只有包内的代码可以访问他们。

例如,我们的 items.go 文件中有个这样的函数:

1
2
3
func NewItem() *Item {
// ...
}

它可以通过 models.NewItem() 这样被调用。但是如果函数命名为 newItem,我们将不能从不同的包访问它了。

去试试更改 shopping 代码中的函数,类型以及字段的名称。例如,如果你将 ItemPrice 字段命名为 price,你应该会获得一个错误。

包管理

我们用来 buildrungo 命令有一个 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
2
3
import (
"github.com/mattn/go-sqlite3"
)

我知道这看起来像一个 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。这是一个较大的问题如果你有两个项目需要同一个库的不同版本。

为了解决这个问题,你可以使用一个第三方的依赖管理工具。他们仍然很年轻,但 goopgodep 是可信的。更多完整的列表在 go-wiki

接口

接口是定义了合约但并没有实现的类型。举个例子:

1
2
3
type Logger interface {
Log(message string)
}

那这样做有什么作用呢?其实,接口有助于将代码与特定的实现进行分离。例如,我们可能有各种类型的日志记录器:

1
2
3
type SqlLogger struct { ... }
type ConsoleLogger struct { ... }
type FileLogger struct { ... }

针对接口而不是具体实现的编程会使我们很轻松的修改(或者测试)任何代码都不会产生影响。

你会怎么用?就像任何其它类型一样,它结构可以这样:

1
2
3
type Server struct {
logger Logger
}

或者是一个函数参数(或者返回值):

1
2
3
func process(logger Logger) {
logger.Log("hello!")
}

在像 C# 或者 Java 这类语言中,当类实现接口时,我们必须显式的:

1
2
3
4
5
public class ConsoleLogger : Logger {
public void Logger(message string) {
Console.WriteLine(message)
}
}

在 Go 中,下面的情况是隐式发生的。如果你的结构体有一个函数名为 Log 且它有一个 string 类型的参数并没有返回值,那么这个结构体被视为 Logger 。这减少了使用接口的冗长:

1
2
3
4
type ConsoleLogger struct {}
func (l ConsoleLogger) Log(message string) {
fmt.Println(message)
}

Go 倾向于使用专注的接口。Go 的标准库基本上由接口组成。像 io 包有一些常用的接口诸如 io.Readerio.Writerio.Closer 等。如果你编写的函数只需要一个能调用 Close() 的参数,那么你应该接受一个 io.Closer 而不是像 io 这样的父类型。
接口可以成为其他接口的一部分,也就是说接口也可以与其他接口组成新的接口。例如, io.ReadCLoser 的接口是由 io.Reader 接口和 io.Closer 接口组成的。

最后,接口通常用于避免循环导入。由于它们没有具体的实现,因此它们的依赖是有限的。

继续之前

最后,如何围绕 Go 的工作区间构建你的代码,你只有在写了几个非测试的项目之后才会适应。最重要的是记着包名和目录结构之间的紧密关系(不仅仅在一个项目之内,而是整个工作区间)。

Go 处理类型可见性也是简单有效,而且也是一致的。有一些我们没看过的东西,比如常量和全局变量,但是放心,他们的可见性仍有相同的命名规则决定。

最后,如果你初次接触接口,你可能需要花点时间理解他们。然而,当你第一次看到一个期望类似 io.Reader 的函数时,你会发现自己很感谢作者没有要求他或者她需要的东西。

第五章 · 花絮

这章中,我们将讨论 Go 功能杂记,放在其他地方都不太合适。

错误处理

Go 首选错误处理方式是返回值,而不是异常。考虑 strconv.Atoi 函数,它将接受一个字符串然后将它转换为一个整数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"os"
"strconv"
)

func main() {
if len(os.Args) != 2 {
os.Exit(1)
}

n, err := strconv.Atoi(os.Args[1])
if err != nil {
fmt.Println("not a valid number")
} else {
fmt.Println(n)
}
}

你可以创建你自己的错误类型。唯一的要求是你必须实现内建 error 接口的契约:

1
2
3
type error interface {
Error() string
}

更一般地,我们可以通过导入 error 包然后使用它的 New 函数创建我们自己的错误:

1
2
3
4
5
6
7
8
9
10
11
12
import (
"errors"
)


func process(count int) error {
if count < 1 {
return errors.New("Invalid count")
}
...
return nil
}

Go 标准库中有一个使用 error 变量的通用模式。例如, io 包中有一个 EOF 变量它是这样定义的:

1
var EOF = errors.New("EOF")

这是一个包级别的变量(被定义在函数之外),可以被其他包访问(首字母大写)。各种函数可以返回这个错误,例如,当我们从一个文件或者 STDIN 读取时。如果它具有上下文意义,那么您应该使用此错误。作为调用者,我们可以这样使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"io"
)

func main() {
var input int
_, err := fmt.Scan(&input)
if err == io.EOF {
fmt.Println("no more input!")
}
}

作为最后一点,Go 确实有 panicrecover 函数。 panic 就像抛出异常,而 recover 就像 catch,它们很少使用。

Defer

尽管 Go 有一个垃圾回收器,一些资源仍然需要我们显示地释放他们。例如,我们需要在使用完文件之后 Close() 他们。这种代码总是很危险。一方面来说,当我们在写一个函数的时候,很容易忘记关闭我们声明了 10 行的东西。另一方面,一个函数可能有多个返回点。Go 给出的解决方案是使用 defer 关键字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"os"
)

func main() {
file, err := os.Open("a_file_to_read")
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
// 读取文件
}

如果你尝试运行上面的代码,你将会得到错误(文件不存在)。这里只是演示 defer 如何工作。无论什么情况,在函数返回之后(本例中为 main() ),defer 将被执行。这使您可以在初始化的位置附近释放资源并处理多个返回点。

go语言风格

大多数 Go 程序遵循相同的格式化规则,换句话说,一个 tab 键用于缩进,左括号和他们的声明语句在同一行。

我知道,你可能有自己的风格,并且想坚持它。这也是我长期以来所做的事情,但我很高兴我最终放弃了。一个大原因是 go fmt 命令。它易于使用而且具有权威性(所以就没有人争论无意义的偏好)。

当你在一个项目内的时候,你可以运用格式化规则到这个项目及其所有子目录:

1
go fmt ./...

试一试,它不仅缩进你的代码,也对齐了声明的字段和按字母书序导入。

初始化的 if

Go 对 if 语句做了稍微修改,支持在条件语句被求值之前先进行初始化:

1
2
3
if x := 10; count > x {
...
}

这是一个比较蠢的例子,更现实的是,你可能会像下面这样做:

1
2
3
if err := process(); err != nil {
return err
}

有意思的是,虽然 err 不能在 if 语句之外使用,但他可以在任何 else if 或者 else 之内使用。

空接口和转化

在大多数面向对象的语言中,经常有一个内建的叫 object 的基类,是所有其他类的超类。Go 没有继承,也没有这样一个超类。不过他确实有一个没有任何方法的空接口: interface{}。因为空接口没有方法,可以说所有类型都实现了空接口,并且由于空接口是隐式实现的,因此每种类型都满足空接口契约。

如果我们像,我们可以定义如下签名的 add 函数:

1
2
3
func add(a interface{}, b interface{}) interface{} {
...
}

为了将一个接口变量转化为一个显式的类型,又可以用 .(TYPE)

1
return a.(int) + b.(int)

提醒,如果底层类型不是 int,上面的结果将是 error。

你也可以访问强大的类型转换:

1
2
3
4
5
6
7
8
switch a.(type) {
case int:
fmt.Printf("a is now an int and equals %d\n", a)
case bool, string:
// ...
default:
// ...
}

你将会看到,使用空接口可能超出了你的期望。但是虽然它将让代码看起来不那么好看,来回转换代码有时看起来也很丑陋并且危险,但在一个静态语言中,它是唯一的选择。

字符串和字节数组

字符串和字节数组是紧密相关的。我们可以轻松地在他们之间转换:

1
2
3
stra := "the spice must flow"
byts := []byte(stra)
strb := string(byts)

实际上,这种转换方式在各种类型之间是通用的。一些函数显示地需要一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
)

type Add func(a int, b int) int

func main() {
fmt.Println(process(func(a int, b int) int{
return a + b
}))
}

func process(adder Add) int {
return adder(1, 2)
}

这样使用函数会帮助我们从具实现中解耦代码,更像在使用接口实现。

继续之前

我们研究了使用 Go 编程的各个方面,最值得注意的是,我们看到了错误的处理行为以及如何释放连接和打开的文件资源。许多人不喜欢 Go 的错误处理方法。这感觉像是倒退了一步。 有时,我同意。然而,我也发现这会让代码更容易理解。 defer 是一种不寻常但是实用的资源管理方法。事实上,它不仅限于资源管理。您可以将 defer 用于任何目的, 比如函数退出时的日志记录。

当然,我们还没有看到 Go 提供的所有花絮,但在你解决遇到的任何问题时你应该感到足够舒服。

第六章 · 并发

Go 通常被描述为一种并发友好的语言。 原因是它提供了两种强大机制的简单语法: 协程通道

Go协程

协程 类似于一个线程,但是由 Go 而不是操作系统预定。在 协程 中运行的代码可以与其他代码同时运行。我们来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"time"
)

func main() {
fmt.Println("start")
go process()
time.Sleep(time.Millisecond * 10) // this is bad, don't do this!
fmt.Println("done")
}

func process() {
fmt.Println("processing")
}

这里有一些有趣的事情, 但最重要的是我们如何开始一个 协程 。 我们只需使用 go 关键字,然后使用我们想要执行的函数。如果我们只想运行一部分代码, 如上所述, 我们可以使用匿名函数。需要注意的是,匿名函数不只是可以在 协程 中使用,其他地方也可以。

1
2
3
go func() {
fmt.Println("processing")
}()

协程 易于创建且开销很小。最终多个 协程 将会在同一个底层的操作系统线程上运行。这通常也称为 M:N 线程模型,因为我们有 M 个应用线程( 协程 )运行在 N 个操作系统线程上。结果就是,一个 协程 的开销和系统线程比起来相对很低(几KB)。在现代的硬件上,有可能拥有数百万个 协程

此外,这里还隐藏了映射和调度的复杂性。我们只需要说 这段代码需要同时并发执行 然后让 Go 自己去实现它。

如果我们回到我们的例子中,你将会注意到我们使用 Sleep 让程序等了几毫秒。这是因为主进程在退出前 协程 才会有机会去执行(主进程在退出前不会等待全部 协程 执行完毕)。要解决这个问题,我们需要协调我们的代码。

同步

创建一个协程是微不足道的, 它们开销很小我们可以启动很多; 但是,需要协调并发代码。为了解决这个问题, Go 提供了 通道。 在我们学习 通道 之前,我认为了解并发编程的基础知识非常重要。

编写并发代码要求您特别注意在哪里读取和写入一个值。 在某些方面, 例如没有垃圾回收的语言 – 它需要您从一个新的角度去考虑您的数据,始终警惕着可能存在的危险。 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"time"
)

var counter = 0

func main() {
for i := 0; i < 20; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}

func incr() {
counter++
fmt.Println(counter)
}

你觉得将会输出什么呢?

如果你认为输出的是 1, 2, ... 20 这既不对也没错。如果你运行了以上的代码确实可能得到这个输出。可是,这个操作就很让人懵逼的。 啥?因为我们可能有多个 (这个情况下两个) 协程 同时写入一个相同变量 counter 。或者,同样糟糕的是,一个协程要读取 counter 时,另一个协程正在写入。

这个真的很危险吗?当然啦! counter++ 看起来可能是一行很简单的代码,但它是实际上被拆分为多个汇编语句 – 确切的性质依赖于你跑程序的平台。如果你运行这个例子,你将经常看到那些数字是以一种乱七八糟的顺序打印的,亦或数字是重复的/丢失的。别着急还会有更糟糕的情况, 比方说系统崩溃或者访问并增加任意区块的数据!

从变量中读取变量是唯一安全的并发处理变量的方式。 你可以有想要多少就多少的读取者, 但是写操作必须要得同步。 有太多的方法可以做到这个了,包括使用一些依赖于特殊的 CPU 指令集的真原子操作。然而, 常用的操作还是使用互斥量(译者注:mutex):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"time"
"sync"
)

var (
counter = 0
lock sync.Mutex
)

func main() {
for i := 0; i < 20; i++ {
go incr()
}
time.Sleep(time.Millisecond * 10)
}

func incr() {
lock.Lock()
defer lock.Unlock()
counter++
fmt.Println(counter)
}

互斥量序列化会锁住锁下的代码访问。因为默认的的 sync.Mutex 是未锁定状态,这儿我们就得先定义 lock sync.Mutex

这操作是不看着超简单? 这个例子是具有欺骗性的。当我们进行并发编程时会产生一系列严重的 Bug。 首先,并不是经常能很明显知道什么代码需要保护。使用这样粗糙的锁操作(覆盖着大量代码的锁操作)确实很诱人,这就违背了我们当初进行并发编程的初心了。 我们肯定是需要个优雅的锁操作; 否则,我们最终会把多条快速通道走成单车道的。

另外一个问题是与死锁有关。 使用单个锁时,这没有问题,但是如果你在代码中使用两个或者更多的锁,很容易出现一种危险的情况,当协程A拥有锁 lockA **,想去访问锁 **lockB **,同时协程B拥有锁 **lockB 并需要访问锁 lockA

实际上我们使用一个锁时也有可能发生死锁的问题,就是当我们忘记释放它时。 但是这和多个锁引起的死锁行为相比起来,这并不像多锁死锁那样危险(因为这真的 很难发现),当你试着运行下面的代码时,您可以看见发生了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"time"
"sync"
)

var (
lock sync.Mutex
)

func main() {
go func() { lock.Lock() }()
time.Sleep(time.Millisecond * 10)
lock.Lock()
}

到现在为止还有很多并发编程我们没有看到过。 首先,有一个常见的锁叫读写互斥锁。它主要提供了两种锁功能: 一个锁定读取和一个锁定写入。它的区别是允许多个同时读取,同时确保写入是独占的。在 Go 中, sync.RWMutex 就是这种锁。另外 sync.Mutex 结构不但提供了LockUnlock 方法 ,也提供了RLockRUnlock 方法;其中 R 代表 Read.。虽然读写锁很常用,它也给开发人员带来了额外的负担:我们不但要关注我们正在访问的数据,还要注意如何访问。

此外,部分并发编程不只是通过为数不多的代码按顺序的访问变量; 它也需要协调多个协程。 例如,休眠10毫秒并不是一个特别优雅的解决方案。如果一个协程消耗的时间需要超过10毫秒怎么办?如果协程消耗更少的时间而我们浪费周期怎么办?又或者可以等待协程运行完毕, 我们想另外一个协程 嗨, 我有新的数据需要你处理?

这些事在没有 通道 的情况下都是可以完成的。当然对于更简单的情况,我相信你应该 应该 使用基本的功能比如 sync.Mutexsync.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
2
3
4
5
6
7
8
9
10
type Worker struct {
id int
}

func (w Worker) process(c chan int) {
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
}
}

我们的 worker 是简单的。他一直等到数据可用然后处理它。尽职尽责,它一直在一个循环中做这个,永远等待更多的数据去处理。

为了去用这个,第一件事情是启动一些 workers:

1
2
3
4
5
c := make(chan int)
for i := 0; i < 5; i++ {
worker := &Worker{id: i}
go worker.process(c)
}

然后,给这些 worker 一些活干:

1
2
3
4
for {
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}

这里有一个完整的可运行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"time"
"math/rand"
)

func main() {
c := make(chan int)
for i := 0; i < 5; i++ {
worker := &Worker{id: i}
go worker.process(c)
}

for {
c <- rand.Int()
time.Sleep(time.Millisecond * 50)
}
}

type Worker struct {
id int
}

func (w *Worker) process(c chan int) {
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
}
}

我们不知道哪个 worker 将得到什么数据。但我们能确保的是 Go 保证了发送到通道的数据只会被一个接收器接收。

记着,唯一的共享状态时通道,我们可以安全地同时从它接收和发送数据。通道提供了所有我们需要的同步代码保证,在任何时间只有一个 goroutine 可以访问特定的数据。

缓冲通道

上面给出的代码中,如果有超过能处理的数据到来会发什么?你可以通过更改 worker 接收到数据之后的暂停时间来模拟这个。

1
2
3
4
5
for {
data := <-c
fmt.Printf("worker %d got %d\n", w.id, data)
time.Sleep(time.Millisecond * 500)
}

我们的主代码中发生的是,接收用户数据的代码(刚刚使用随机数生成器模拟的)是阻塞,因为没有接收器可用。

在某些情况下,你可能需要担保数据被处理掉,这个时候就需要开始阻塞客户端。在某些情况下,你可能会降低这种担保。这有几种常用的策略实现它。第一个就是缓冲数据。如果没有worker可用,我们想去临时存储数据在某些队列中。通道内建这种缓冲容量,当我们使用 make 创建通道的时候,可以设置通道的长度:

1
c := make(chan int, 100)

你可以对此更改进行更改,但你会注意到处理仍然不稳定。缓冲通道不会增加容量,他们只提供待处理工作的队列,以及处理突然飙升的任务量的好方法。在我们的示例中,我们不断推送比 worker 可以处理的数据更多的数据。

然而,我们实际上可以通过查看通道的 len 来理解缓冲通道是什么。

1
2
3
4
5
for {
c <- rand.Int()
fmt.Println(len(c))
time.Sleep(time.Millisecond * 50)
}

你可以看到通道长度一直增加直到满了,这个时候往我们的通道发送数据将再一次阻塞。

Select

即使有缓冲,在某些时候我们需要开始删除消息。我们不能为了让 worker 轻松而耗尽所有内存。为了实现这个,我们使用 Go 的 select

语法上,select 看起来有一点像 switch。使用它,我们提供当通道不能发送数据的时候处理代码。首先,让我们移除通道缓冲来看看 select 如何工作:

1
c := make(chan int)

接下来,改变我们的 for 循环:

1
2
3
4
5
6
7
8
9
10
for {
select {
case c <- rand.Int():
// 可选的代码在这里
default:
// 这里可以留空以静默删除数据
fmt.Println("dropped")
}
time.Sleep(time.Millisecond * 50)
}

我们将每秒推送20条消息,但是我们的 worker 每秒仅仅能处理10条。也就是说,一般的消息,将被丢掉。

这只是我们能使用 select 实现的一个开始。select 的主要目的是管理多个通道,select 将阻塞直到第一个通道可用。如果没有通道可用,如果提供了 default ,那么他就会被执行。如果多个通道都可用了,随机挑选一个。

很难用一个简单的例子来证明这个行为,因为它是一个相当高级的功能。下一节可能有助于证明这个。

超时

我们看过了缓冲消息以及简单地将他们丢弃。另一个通用的选择是去超时。我们将阻塞一段时间,但不会永远。这在 Go 中也是很容易实现的。虽然,语法很难遵循,但是这样一个简洁有用的功能我不能将它排除在外。

为了阻塞最长时间,我们可以用 time.After 函数。我们来一起看看它并试着超越魔法。为了去用这个,我们的发送器将变成:

1
2
3
4
5
6
7
8
for {
select {
case c <- rand.Int():
case <-time.After(time.Millisecond * 100):
fmt.Println("timed out")
}
time.Sleep(time.Millisecond * 50)
}

time.After 返回了一个通道,所以我们在 select 中使用它。这个通道可以在指定时间之后被写入。就这样,没有其他魔法了。如果你比较好奇,这里有一个 after 的实现,看起来大概就是这个样子咯:

1
2
3
4
5
6
7
8
func after(d time.Duration) chan bool {
c := make(chan bool)
go func() {
time.Sleep(d)
c <- true
}()
return c
}

回到我们的 select,还有两个东西可以试试。首先,如果添加回 default 会发生什么?能猜到吗?试试它。如果你不确定,记着如果没有可用的通道,default 将会立即触发。

还有,time.After 是一个 chan time.Time 类型的通道。上面的例子中,我们仅仅是简单地丢弃掉了发送到通道的值。如果你想要,你可以接受它:

1
2
case t := <-time.After(time.Millisecond * 100):
fmt.Println("timed out at", t)

注意力重新回到我们的 select,可以看到我们发送给 c 但是却从 time.After 接收。无论我们从哪里接收,发送给谁,或者任何通道的组合,select 工作方式是相同的:

  • 第一个可用的通道被选择。
  • 如果多个通道可用,随机选择一个。
  • 如果没有通道可用,default 情况将被执行。
  • 如果没有 default,select 将会阻塞。

最后,在 for 中看到一个 select 是很常见的:

1
2
3
4
5
6
7
8
9
for {
select {
case data := <-c:
fmt.Printf("worker %d got %d\n", w.id, data)
case <-time.After(time.Millisecond * 10):
fmt.Println("Break time")
time.Sleep(time.Second)
}
}

继续之前

如果你是并发编程的新手,那么看起来似乎都是压倒性的。 它绝对需要非常多的关注。Go旨在让它变得更容易。

Goroutines 有效的抽象了我们需要并发执行的代码。通道帮助消除数据共享时共享数据可能发生的一些严重错误。这不仅可以消除错误, 还可以改变并发编程的方式。你只用考虑通过信息传递实现并发编程,而不是危险的代码区域。

话虽如此,我仍然广泛使用 syncsync / atomic 包中的各种同步原语。我觉得比较重要的是通过使用这两种方式比较舒适。我建议你首先关注通道,但是当你遇到一个需要短暂锁的简单示例时,请考虑使用互斥锁或读写互斥锁。

总结

我最近听说 Go 被描述为一个枯燥的语言。枯燥是因为很容易去学,很容易写,以及最重要的,易读。或许,我确实认为这个实现不太好,毕竟,我确实花了三章讨论类型和如何声明变量。

如果你有静态类型语言的工作经历,我们所看到的内容仅仅只是一个复习。Go 使得指针可用性增强,并且切片是数组的包装,对于经验丰富的 Java 或 C#开发人员来说可能并不算是压倒性优势。

如果你曾经大多在使用动态语言,你可能会感到有点不同。它是一个值得学习的东西。其中最重要的是声明和各种初始化的语法。尽管我是 Go 的粉丝,Go 尽管也在简单性方面取得了一些进展,但它并不简单。不过,它归纳为一些基本的规则(比如你只能声明变量一次以及 := 确实声明了变量)以及基本理解(比如 new(X) 或者 &X{} 仅仅只是分配内存,但是切片,映射以及通道需要更多的初始化,所以用 make)。

除了这些,Go 给了我们简单但有效的方式组织我们的代码。接口,基于返回的错误处理,用于资源管理的 defer以及实现组合的简单方式。

最后但也最重要的是内置并发支持。关于 协程 ,除了有效和简单(无论如何简单易用)之外,几乎没有什么可说的了。这是一个很好的抽象。 通道 更为复杂。我一直认为在使用高级包装器之前先理解最基本使用方法。我认为不通过 通道 学习并发编程是很有用的。但是,对我来说,我觉得 通道 的实现方式不像一个简单的抽象。它们几乎都是自己的基本构件。我这样说是因为它们改变了你编写和思考并发编程的方式。 鉴于并发编程有多么困难,这绝对是一件好事。

以及实现组合的简单方式。

最后但也最重要的是内置并发支持。关于 协程 ,除了有效和简单(无论如何简单易用)之外,几乎没有什么可说的了。这是一个很好的抽象。 通道 更为复杂。我一直认为在使用高级包装器之前先理解最基本使用方法。我认为不通过 通道 学习并发编程是很有用的。但是,对我来说,我觉得 通道 的实现方式不像一个简单的抽象。它们几乎都是自己的基本构件。我这样说是因为它们改变了你编写和思考并发编程的方式。 鉴于并发编程有多么困难,这绝对是一件好事。

Git清空所有commit记录方法

说明:例如将代码提交到git仓库,将一些敏感信息提交,所以需要删除提交记录以彻底清除提交信息,以得到一个干净的仓库且代码不变

  1. Checkout

    1
    git checkout –orphan latest_branch
  2. Add all the files

    1
    git add -A
  3. Commit the changes

    1
    git commit -am "commit message"
  4. Delete the branch

    1
    git branch -D master
  5. Rename the current branch to master

    1
    git branch -m master
  6. Finally, force update your repository

    1
    git push -f origin master

解决配置authorized_keys无法登陆

因为权限问题才无法登陆,这里记录下,方便后面查用。

1
2
sudo chmod 644 ~/.ssh/authorized_keys
sudo chmod 700 ~/.ssh

pypi 镜像使用帮助

pypi 镜像每 5 分钟同步一次。

临时使用

1
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package

注意,simple 不能少, 是 https 而不是 http

设为默认

升级 pip 到最新的版本 (>=10.0.0) 后进行配置:

1
2
pip install pip -U
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

如果您到 pip 默认源的网络连接较差,临时使用本镜像站来升级 pip:

1
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U

半径范围内随机经纬度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
private static final double EARTH_RADIUS = 6372.796924;

public static GpsInfo getRandomLocation(GpsInfo center, double distance) {
if (distance <= 0) distance = 50;
double lat, lon, brg;
distance = distance / 1000;
GpsInfo location = new GpsInfo();
double maxdist = distance;
maxdist = maxdist / EARTH_RADIUS;
double startlat = rad(center.getLat());
double startlon = rad(center.getLon());
double cosdif = Math.cos(maxdist) - 1;
double sinstartlat = Math.sin(startlat);
double cosstartlat = Math.cos(startlat);
double dist;
double rad360 = 2 * Math.PI;
dist = Math.acos((new Random().nextDouble() * cosdif + 1));
brg = rad360 * new Random().nextDouble();
lat = Math.asin(sinstartlat * Math.cos(dist) + cosstartlat * Math.sin(dist) * Math.cos(brg));
lon = deg(normalizeLongitude(startlon * 1 + Math.atan2(Math.sin(brg) * Math.sin(dist) * cosstartlat, Math.cos(dist) - sinstartlat * Math.sin(lat))));
lat = deg(lat);

location.setLat(padZeroRight(lat));
location.setLon(padZeroRight(lon));
return location;
}

static double rad(double d) {
return d * Math.PI / 180.0;
}

static double deg(double rd) {
return (rd * 180 / Math.PI);
}

static double normalizeLongitude(double lon) {
double n = Math.PI;
if (lon > n) {
lon = lon - 2 * n;
} else if (lon < -n) {
lon = lon + 2 * n;
}
return lon;
}

static double padZeroRight(double s) {
double sigDigits = 8;
s = Math.round(s * Math.pow(10, sigDigits)) / Math.pow(10, sigDigits);
return s;
}

Golang transaction 事务使用的正确姿势

第一种写法

这种写法非常朴实,程序流程也非常明确,但是事务处理与程序流程嵌入太深,容易遗漏,造成严重的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func DoSomething() (err error) {
tx, err := db.Begin()
if err != nil {
return
}


defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // re-throw panic after Rollback
}
}()


if _, err = tx.Exec(...); err != nil {
tx.Rollback()
return
}
if _, err = tx.Exec(...); err != nil {
tx.Rollback()
return
}
// ...


err = tx.Commit()
return
}

第二种写法

下面这种写法把事务处理从程序流程抽离了出来,不容易遗漏,但是作用域是整个函数,程序流程不是很清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func DoSomething() (err error) {
tx, err := db.Begin()
if err != nil {
return
}


defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // re-throw panic after Rollback
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()


if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
// ...
return
}

第三种写法

写法三是对写法二的进一步封装,写法高级一点,缺点同上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func Transact(db *sql.DB, txFunc func(*sql.Tx) error) (err error) {
tx, err := db.Begin()
if err != nil {
return
}


defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p) // re-throw panic after Rollback
} else if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
}
}()


err = txFunc(tx)
return err
}


func DoSomething() error {
return Transact(db, func (tx *sql.Tx) error {
if _, err := tx.Exec(...); err != nil {
return err
}
if _, err := tx.Exec(...); err != nil {
return err
}
})
}

我的写法

经过总结和实验,我采用了下面这种写法,defer tx.Rollback() 使得事务回滚始终得到执行。 当 tx.Commit() 执行后,tx.Rollback() 起到关闭事务的作用, 当程序因为某个错误中止,tx.Rollback() 起到回滚事务,同事关闭事务的作用。

普通场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func DoSomething() (err error) {
tx, _ := db.Begin()
defer tx.Rollback()

if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
// ...


err = tx.Commit()
return
}

循环场景

(1) 小事务 每次循环提交一次 在循环内部使用这种写法的时候,defer 不能使用,所以要把事务部分抽离到独立的函数当中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func DoSomething() (err error) {
tx, _ := db.Begin()
defer tx.Rollback()

if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
// ...


err = tx.Commit()
return
}


for {
if err := DoSomething(); err != nil{
// ...
}
}

(2) 大事务 批量提交 大事务的场景和普通场景是一样的,没有任何区别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func DoSomething() (err error) {
tx, _ := db.Begin()
defer tx.Rollback()

for{
if _, err = tx.Exec(...); err != nil {
return
}
if _, err = tx.Exec(...); err != nil {
return
}
// ...
}

err = tx.Commit()
return
}

参考链接:
https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback
原文地址:
http://hopehook.com/2017/08/21/golang_transaction/

HexoClient1.2.6版本发布

本次更新内容

  • feature:支持hexo特性front-matter #32 #38
  • bugfix:修复一处RCE(任意代码执行)漏洞 #35
  • 升级electron 到最新版本
  • 升级webpack到最新版本,解决老版本漏洞问题

功能预览

image.png
image.png

相关链接

go mod 的使用

从Go1.11开始,golang官方支持了新的依赖管理工具go mod

命令行说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
➜  ~ go mod
Go mod provides access to operations on modules.

Note that support for modules is built into all the go commands,
not just 'go mod'. For example, day-to-day adding, removing, upgrading,
and downgrading of dependencies should be done using 'go get'.
See 'go help modules' for an overview of module functionality.

Usage:

go mod <command> [arguments]

The commands are:

download download modules to local cache
edit edit go.mod from tools or scripts
graph print module requirement graph
init initialize new module in current directory
tidy add missing and remove unused modules
vendor make vendored copy of dependencies
verify verify dependencies have expected content
why explain why packages or modules are needed

Use "go help mod <command>" for more information about a command.
  • go mod download: 下载依赖的module到本地cache
  • go mod edit: 编辑go.mod
  • go mod graph: 打印模块依赖图
  • go mod init: 在当前目录下初始化go.mod(就是会新建一个go.mod文件)
  • go mod tidy: 整理依赖关系,会添加丢失的module,删除不需要的module
  • go mod vender: 将依赖复制到vendor下
  • go mod verify: 校验依赖
  • go mod why: 解释为什么需要依赖

在新项目中使用

使用go mod并不要求你的项目源码放到$GOPATH下,所以你的新项目可以放到任意你喜欢的路径。在项目根目录下执行go mod init,会生成一个go.mod文件。然后你可以在其中增加你的依赖,如下:

1
2
3
4
5
6
7
8
module github.com/gaoyoubo/xxx

go 1.12

require (
github.com/go-sql-driver/mysql v1.4.1
.... 你的依赖类似这样,添加到这里,一行一条。
)

然后执行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 将会升级到指定的版本

mysql5.x重置密码

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.

PREPARATION

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
2
3
4
5
6
7
8
shell> sudo -s
shell> vi /usr/local/mysql/support-files/my-default.cnf

...
[mysqld]
skip-grant-tables
skip-networking
...

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

RESET THE PASSWORD

After MySQL is started again, you can log into the CLI and reset the password:

1
2
3
shell> mysql -u root
mysql> FLUSH PRIVILEGES;
mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'super-secret-password';

PLAN B

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
2
shell> ps -aef | grep mysql | grep -v grep
74 28017 1 0 Fri10AM ?? 5:59.50 /usr/local/mysql/bin/mysqld --user=_mysql --basedir=/usr/local/mysql --datadir=/usr/local/mysql/data --plugin-dir=/usr/local/mysql/lib/plugin --log-error=/usr/local/mysql/data/mysqld.local.err --pid-file=/usr/local/mysql/data/mysqld.local.pid

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
shell> kill -15 [process id]

In this example, the command would look like this:

1
shell> 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.

CONCLUSION

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.

Golang和Java构建工具调查

Github:https://github.com/blindpirate/report-of-build-tools-for-java-and-golang

A Survey on Build Tools of Golang and Java

Java

Conclusion

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:

trending

Algorithm

  • Clone top 1000 Java repositories to local disk
  • Analyze the repositories by identity files:
Tool Name Identity Files
Gradle build.gradle
Maven pom.xml
Ant build.xml
Npm package.json
Bazel BUILD
Make Makefile/makefile

How

  • Make sure Git/Groovy 2.4+/JDK 1.7+ are installed.
  • Run groovy GithubTopRankCrawler.groovy -l java -d <path to store the 1000 repos> to clone all repositories locally.
  • Run groovy JavaBuildToolScanner.groovy -d <path to store the 1000 repos> to analyze these repos.

Golang

Conclusion

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.

Algorithm

  • Clone top 1000 Go repositories to local disk
  • Analyze the repositories by identity files:
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

How

  • Make sure Git/Groovy 2.4+/JDK 1.7+ are installed.
  • Run 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.
  • Run groovy GoBuildToolScanner.groovy <path to store the 1000 repos> to analyze these repos.

Git仓库初始化

Create a new repository

1
2
3
4
5
6
git clone git@github.com:gaoyoubo/hexo-client.git
cd user-center-transfer
touch README.md
git add README.md
git commit -m "add README"
git push -u origin master

Existing folder

1
2
3
4
5
6
cd existing_folder
git init
git remote add origin git@github.com:gaoyoubo/hexo-client.git
git add .
git commit -m "Initial commit"
git push -u origin master

Existing Git repository

1
2
3
4
5
cd existing_repo
git remote rename origin old-origin
git remote add origin git@github.com:gaoyoubo/hexo-client.git
git push -u origin --all
git push -u origin --tags

JWT介绍

JWT全称为:JSON Web Token是目前最流行的跨域认证的解决方案。

跨域认证的问题

互联网服务离不开用户认证。一般流程是下面这样。

  1. 用户向服务器发送用户名和密码。
  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
  3. 服务器向用户返回一个 session_id,写入用户的 Cookie。
  4. 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
  5. 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

image.png

这种模式的问题在于,扩展性不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

image.png

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

JWT的原理

JWT的原则是在服务器身份验证之后,将生成一个JSON对象并将其发送回用户,如下所示。

1
2
3
4
5
{
"username": "admin",
"role": "admin",
"expire": "2018-12-24 20:15:56"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

JWT的数据结构

实际的 JWT 大概就像下面这样。
image.png

它是一个很长的字符串,中间用点.分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

JWT 的三个部分依次如下。

  • Header (头)
  • Payload (负载)
  • Signature (签名)
    写成一行,就是下面的样子:
    1
    Header.Payload.Signature
    image.png
    下面依次介绍这三个部分。

Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

上面代码中,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

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号
    除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
    1
    2
    3
    4
    5
    {
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
    }
    注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
    这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

Signature 部分是对前两部分的签名,防止数据篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用.分隔,就可以返回给用户。

Base64URL

前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+/=,在 URL 里面有特殊含义,所以要被替换掉:=被省略,+替换成-/替换成_。这就是Base64URL 算法。

JWT的使用方式

客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。

此后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域,所以更好的做法是放在 HTTP 请求的头信息Authorization字段里面。

1
Authorization: Bearer <token>

另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

JWT 的几个特点

  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

参考

IDE Goland DEBUG报错

在升级GO版本到1.11后发现Goland的Debug报错,错误信息如下:

1
could not launch process: decoding dwarf section info at offset 0x0: too short

原因是Goland的dlv不是新版本,导致不能debug调试。

解决办法:

  1. 更新dlv:go get -u github.com/derekparker/delve/cmd/dlv
  2. 修改goland配置,Help->Edit Custom Properties中增加新版dlv的路径配置:dlv.path=$GOPATH/bin/dlv
  3. 重启Goland,再次使用debug调试工具,就没有问题了。

参考:https://stackoverflow.com/questions/43014884/mac-osx-jetbrains-gogland-delve-debugging-meet-could-not-launch-process-could/43014980#43014980

HexoClient使用帮助

简介

HexoClient是一款跨平台的Hexo管理工具。

项目地址:https://github.com/gaoyoubo/hexo-client

QQ群

欢迎加入HexoClient用户群交流。

  • QQ群号:618213781
  • QQ群二维码

项目背景

我是从2011年开始写博客,在早期的时候wordpresszblogemlog等开源的博客程序都是用过。但是本着生命在于则疼的原则,后来我自己使用Java写了个简单的Blog程序( https://gitee.com/gaoyoubo/mlog ) 将其托管在阿里云服务器上。但是后面觉得为了一个博客单独买一台服务器成本比较高,所以后来改用Hexo+Github Pages,这样每年基本只需要几十块钱的域名费用即可。开始使用Hexo的时候也只是按照常规方式使用,后来了解到了electron框架,所以决定利用electron来为hexo写一个客户端。开始完全是为了自用,开源出去之后反响还不错,收到很多hexo博客党的反馈。

使用帮助

阅读前提

本文不会讲解如何安装、配置、使用Hexo,所以阅读前请确保掌握以下技能。

  • 能独立安装使用Hexo
  • 能够正确的将Hexo部署到GitHub Pages
  • 熟练掌握markdown语法
  • 了解基本的Git用法

安装

第一步

首先安装hexo

1
2
3
4
5
npm install hexo-cli -g
hexo init blog
cd blog
npm install
hexo server

第二步

去Hexo的产品发布页( https://github.com/gaoyoubo/hexo-client/releases )下载你对应平台的安装包进行安装。

第三部

成功安装后打开程序会要求弹窗要求填写Hexo项目路径,该路径就是第一步通过hexo init blog创建的博客路径。正确配置路径之后即可愉快的使用HexoClient。

利用Travis-CI实现自动部署

原理概述

我在Github上创建以下两个项目:

  • blog.mspring.org 该项目开启GitHub Pages用来存放hexo deploy之后的静态网页
  • blog-source 该项目用来存放我的hexo原始项目(也就是你通过hexo init创建的工程)
    然后我们利用Trvais-ci,进行自动构建和发布。当Travis-ci监控到blog-source有新的提交记录,那么会自动执行脚本将更新发布到blog.mspring.org

使用Travis-CI自动发布

第一步:生成access token

进入Github个人主页,找到:Settings -> Developer settings -> Personal access tokens,然后取Generate new token,参照下图配置即可。

这里生成的Token,接下来会用到,请先妥善保存好。

第二步:注册并开启Travis-CI项目构建

使用 GitHub账户登录 Travis-CI官网 ,进去后能看到已经自动关联了 GitHub 上的仓库。这里我们选择需要启用的项目,即 blog-source。然后点击后面的Settings进入设置界面。

第三步:配置Travis-CI自动构建

进入设置界面后可以参考我的配置:

配置主要注意一下两点即可:

  • Build pushed branches

当分支收到新的push之后构建

  • Environment Variables -> GH_TOKEN

GH_TOKEN,是我们第一步在github中生成的access token,因为要从github上将代码拉到travis-ci机器上进行构建,所以需要该token授权。

第四步:配置hexo的_config.yml

因为我们的博客托管在github pages,所以我们是以git的方式进行deploy的,hexo如何配置使用git方式进行deploy,请自行Google。下面截取了我的_config.yml文件中关于git deploy配置的片段。

1
2
3
4
5
6
7
# Deployment
## Docs: https://hexo.io/docs/deployment.html
deploy:
- type: git
# 下方的GH_TOKEN会被.travis.yml中sed命令替换
repo: https://GH_TOKEN@github.com/gaoyoubo/blog.mspring.org.git
branch: master

第五步:配置构建脚本.travis.yml

在hexo项目的根目录创建.travis.yml文件,该文件就是travis的构建脚本,下面是我的脚本配置,我会在脚本中详细注释每一步的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 指定语言为node_js,nodejs版本stable
language: node_js
node_js: stable

# 指定构建的分支
branches:
only:
- master

# 指定node_modules缓存
cache:
directories:
- node_modules

# 构建之前安装hexo-cli,因为接下来会用到
before_install:
- npm install -g hexo-cli

# 安装依赖
install:
- npm install

# 执行脚本,先hexo clean 再 hexo generate,会使用hexo的同学应该不陌生。
script:
- hexo clean
- hexo generate

# 上面的脚本执行成功之后执行以下脚本进行deploy
after_success:
- git init
- git config --global user.name "GaoYoubo"
- git config --global user.email "gaoyoubo@foxmail.com"
# 替换同目录下的_config.yml文件中GH_TOKEN字符串为travis后台配置的变量
- sed -i "s/GH_TOKEN/${GH_TOKEN}/g" ./_config.yml
- hexo deploy

使用HexoClient管理你的文章

  • HexoClient在启动之后选择好hexo安装的目录,会自动读取Hexo目录中的文章。
  • HexoClient中支持新建、修改文章,新建修改文章之后点击发布按钮能够将文章更改提交到git,并自动通过travis自动发布。(前提是按照上面步骤配置好)
  • HexoClient支持七牛图片上传,七牛10G存储空间,每月10G流量免费,可以自行注册配置七牛,配置好后将七牛的ak、sk、bucket、域名配置到HexoClient中

常见问题

  • 出现莫名其妙的未知错误怎么办

在菜单栏中找到:查看 -> 切换开发者工具,将开发者工具打开,然后看控制台是否有错误,如果有错误将错误信息copy出来,点击这里提交问题:https://github.com/gaoyoubo/hexo-client/issues/new

  • Hexo中有文章,但是打开之后却显示空白

HexoClient的数据加载是完全依赖于Hexo的,所以在打开HexoClient之前要确保你的Hexo是install成功的。

HexoClient更新记录

v1.3.2 (2019-08-26)

  • 新增好博客导航功能,搜集和推荐优质技术博客(欢迎自荐和推荐优质博客)
  • 添加QQ交流群(QQ群:618213781)

v1.3.1 (2019-08-12)

  • 修复检查更新提示错误。#64
  • 修复Windows系统下的一个样式错误。#65

v1.3.0 (2019-08-02)

  • 修复阿里云oss图片上传后url不正确的问题。#60
  • 支持一键调用hexo generate -d命令发布文章,thanks EVINK
    image.png

v1.2.9 (2019-07-19)

  • 支持草稿功能
  • 支持检查更新功能
  • 修复创建文章时ctrl+s多次保存会生成多篇文章的问题
  • 修复选中分类、标签展示之后从其他页面切换回来选中状态丢失的问题

v1.2.8 (2019-07-16)

v1.2.7 (2019-05-15)

v1.2.6 (2019-03-15)

  • feature:支持hexo特性front-matter #32 #38
  • bugfix:修复一处RCE(任意代码执行)漏洞 #35
  • 升级electron 到最新版本
  • 升级webpack到最新版本,解决老版本漏洞问题

v1.2.5 (2019-01-29)

  • bugfix

v1.2.4 (2019-01-24)

  • 新增分类标签导航
  • 支持自定义文章路径
  • 修复若干BUG

v1.2.3 (2019-01-02)

  • 支持i18n
  • 新增sm.ms图床
  • 支持上传粘贴板图片
  • 优化设置页面布局
  • 修复发布时仅支持master分支的问题

v1.2.2 (2018-12-04)

  • 支持文章搜索
  • 优化新建、编辑文章页布局
  • 优化调整发布功能按钮
  • 支持新建文章、发布快捷键操作
  • 其他页面细节优化

v1.2.1

  • MacOS下无边框样式
  • 调整菜单栏布局
  • 修改UI配色和界面细节
  • 修复初始化时选择hexo目录失败的问题
  • 升级electron版本到3.x
  • 其他细节修改

v1.1.3

  • 升级markdown编辑器,使用mavonEditor编辑器(https://github.com/hinesboy/mavonEditor)。
  • 修复图片文章列表过长是,切换页面滚动位置丢失的问题。
  • 重构代码,优化调用逻辑和布局层级关系。
  • 升级electron版本到2.0.6。

v1.1.0

  • 优化页面配色。
  • 优化文章预览、详情页面展示样式。
  • 文章内容修改后离开页面进行友好提示。
  • 支持hexo generate 和 hexo deploy。

使用AppVeyor和Travis自动编译Electron全平台应用

package.json配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
{
"name": "HexoClient",
"version": "1.2.2",
"author": "......",
"description": "Hexo 桌面客户端",
"license": "Apache License, Version 2.0",
"homepage": "https://github.com/gaoyoubo/hexo-client",
"repository": {
"type": "git",
"url": "https://github.com/gaoyoubo/hexo-client.git"
},
"main": "./dist/electron/main.js",
"scripts": {
"build": "node .electron-vue/build.js && electron-builder --publish onTagOrDraft"
......
},
"build": {
"appId": "org.mspring.hexo.client",
"productName": "HexoClient",
"directories": {
"output": "build"
},
"files": [
"dist/electron/**/*",
"build/icons/*"
],
"publish": {
"provider": "github",
"owner": "gaoyoubo",
"repo": "hexo-client"
},
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"mac": {
"icon": "build/icons/icon.icns"
},
"win": {
"target": "nsis",
"icon": "build/icons/icon.ico"
},
"linux": {
"category": "Utility",
"target": [
{
"target": "AppImage",
"arch": [
"x64",
"ia32"
]
},
{
"target": "deb",
"arch": [
"x64",
"ia32"
]
},
{
"target": "rpm",
"arch": [
"x64",
"ia32"
]
}
],
"icon": "build/icons"
}
},
......
}

build配置

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

参考:

HexoClient1.2.1版本发布

更新内容

  • 支持文章搜索
  • 优化新建、编辑文章页布局
  • 优化调整发布功能按钮 issues
  • 支持新建文章、发布快捷键操作
  • 其他页面细节优化

发布地址

https://github.com/gaoyoubo/hexo-client/releases

之前版本都只编译了Macos版本安装包,这次特意安装了一个虚拟机将Windows版本也编译了一份。

功能预览




Ubuntu配置Shadownsocks以及配置pac规则

周末没事将自己闲置的Thinkpad安装了最新的Ubuntu18.10版本,安装成功之后就想着将之前在自己的vps上配置的shadowsock服务使用上。

第一步安装shadowsocks

1
sudo apt-get install shadowsocks

第二步配置shadowsocks

安装完成之后默认的配置文件在/etc/shadowsocks/local.json,去将里面的配置修改成自己的即可。

1
2
3
4
5
6
7
8
9
10
11
12
{
"server":"xxx.xxx.xxx.xxx",
"server_port":xxx,
"local_address": "127.0.0.1",
"local_port":1080,
"password":"xxx",
"timeout":300,
"method":"aes-256-cfb",
"fast_open": true,
"workers": 1,
"prefer_ipv6": false
}

第三步启动shadowsocks

1
sudo sslocal -c /etc/shadowsocks/local.json -d start

第四步配置pac规则

1. 安装GenPac

1
2
sudo pip install genpac
pip install --upgrade genpac

2. 新建pac配置存放目录

用来存放用户自定义规则列表文件user-rules.txt和生成后的autoproxy.pac文件,例如我的放在home目录下

1
2
3
mkdir ~/soft/pac
cd ~./soft/pac
touch user-rules.txt

3. 生成autoproxy.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"

4. 配置系统代理

去Ubuntu设置 -> 网络 -> 代理设置设置代理,选择自动,配置url填写你本地的文件路径,例如:file:///home/xxx/soft/pac/autoproxy.pac

Ubuntu18.10配置rc.local自启动

Ubuntu新版本中已经不使用rc.local这种自启动方式了,熟悉旧版本的同学肯定是很不习惯的。那么新版本中如何配置rc.local呢。

  1. 自行创建 /etc/rc.local 添加以下默认内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/bin/sh -e
    #
    # rc.local
    #
    # This script is executed at the end of each multiuser runlevel.
    # Make sure that the script will "exit 0" on success or any other
    # value on error.
    #
    # In order to enable or disable this script just change the execution
    # bits.
    #
    # By default this script does nothing.

    exit 0
  2. 在 exit 0 之前加入自定义内容

  3. 执行以下命令确保 rc.local 开机自启

    1
    2
    3
    sudo chown root:root /etc/rc.local
    sudo chmod 755 /etc/rc.local
    sudo systemctl enable rc-local.service

Hexo1.2.0发布,全新的UI布局

Hexo1.2.0发布,全新的UI布局。

之前一直觉得HexoClient的ui太不像一个原生应用,一眼就能看出是网页做的。同样是基于electron,vscode、atom等应用的ui看起来就特别正规、好看,所以这次周末抽一天时间调整一下UI。

本次更新内容

  • MacOS下无边框样式
  • 调整菜单栏布局
  • 修改UI配色和界面细节
  • 修复初始化时选择hexo目录失败的问题
  • 升级electron版本到3.x
  • 其他细节修改

下载地址

Mac版:https://pan.baidu.com/s/1E-5KrusoBuFGRTunTBqPJQ 提取码:2v2q
Windows版本:请自行源码编译。
Linux版本:请自行源码编译。

项目地址

Github: https://github.com/gaoyoubo/hexo-client
码云:https://gitee.com/gaoyoubo/hexo-client

TODO

  • 自动初始化hexo
  • 文章筛选、搜索

搬瓦工使用笔记

之前一直使用朋友的vpn进行翻墙,今天突然发现用不了了,同事强烈推荐使用搬瓦工https://bwh8.net ),他们反馈很稳定。于是就去官网上买了一个最低配的,配置如下:

1
2
3
4
5
6
SSD: 10 GB RAID-10
RAM: 512 MB
CPU: 1x Intel Xeon
Transfer: 500 GB/mo
Link speed: 1 Gigabit
Multiple locations

价格:$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



Java学习资料

前几天突然有个姑娘加我的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

账号中心服务线上稳定一年纪念

这台服务由三台机器负载,每日处理一亿四千万+次请求,线上稳定运营一年多,未出现任何故障,如果不是这次需求变动还能继续稳定运行。在重启县截图纪念一下。


TOP 10开源的推荐系统简介

转载自:http://ibillxia.github.io/blog/2014/03/10/top-10-open-source-recommendation-systems/

最近这两年推荐系统特别火,本文搜集整理了一些比较好的开源推荐系统,即有轻量级的适用于做研究的SVDFeature、LibMF、LibFM等,也有重量级的适用于工业系统的 Mahout、Oryx、EasyRecd等

SVDFeature

主页: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。

LibMF

主页: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)。

LibFM

主页: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)等算法。

Lenskit

主页: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

主页: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等算法。

Mahout

主页: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等。

Myrrix

主页:http://myrrix.com/ 语言:Java
Myrrix最初是Mahout的作者之一Sean Owen基于Mahout开发的一个试验性质的推荐系统。目前Myrrix已经是一个完整的、实时的、可扩展的集群和推荐系统,主要 架构分为两部分:服务层:在线服务,响应请求、数据读入、提供实时推荐;计算层:用于分布式离线计算,在后台使用分布式机器学习算法为服务层更新机器学习 模型。Myrrix使用这两个层构建了一个完整的推荐系统,服务层是一个HTTP服务器,能够接收更新,并在毫秒级别内计算出更新结果。服务层可以单独使用,无需 计算层,它会在本地运行机器学习算法。计算层也可以单独使用,其本质是一系列的Hadoop jobs。目前Myrrix以被 Cloudera 并入Oryx项目。

EasyRec

主页:http://easyrec.org/ 语言:Java
EasyRec是一个易集成、易扩展、功能强大且具有可视化管理的推荐系统,更像一个完整的推荐产品,包括了数据录入模块、管理模块、推荐挖掘、离线分析等。 EasyRec可以同时给多个不同的网站提供推荐服务,通过tenant来区分不同的网站。架设EasyRec服务器,为网站申请tenant,通过tenant就可以很方便的集成到 网站中。通过各种不同的数据收集(view,buy.rating)API收集到网站的用户行为,EasyRec通过离线分析,就可以产生推荐信息,您的网站就可以通过 Recommendations和Community Rankings来进行推荐业务的实现。

Waffles

主页:http://waffles.sourceforge.net/ 语言:C++
Waffles英文原意是蜂蜜甜饼,在这里却指代一个非常强大的机器学习的开源工具包。Waffles里包含的算法特别多,涉及机器学习的方方面面,推荐系统位于 其中的Waffles_recommend tool,大概只占整个Waffles的1/10的内容,其它还有分类、聚类、采样、降维、数据可视化、音频处理等许许多多工具包,估计 能与之媲美的也就数Weka了。

RapidMiner

主页: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及其改进算法等。

参考资料

How to build tesseract 4 beta on macOS

转载这篇文章之后找到了官方的文档,建议官方文档,官方文档描述更全面。官方文档地址:https://github.com/tesseract-ocr/tesseract/wiki/Compiling

1
2
3
4
brew info tesseract

tesseract: stable 3.05.01 (bottled), HEAD
OCR (Optical Character Recognition) engine

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

Install dependencies

1
2
3
4
5
brew install automake autoconf autoconf-archive libtool
brew install pkgconfig
brew install icu4c
brew install leptonica
brew install gcc

Compile

1
2
3
4
5
6
git clone https://github.com/tesseract-ocr/tesseract/
cd tesseract
./autogen.sh
./configure CC=gcc CXX=g++ CPPFLAGS=-I/usr/local/opt/icu4c/include LDFLAGS=-L/usr/local/opt/icu4c/lib
make -j
make install

Their best trained modes, download the language chi_sim.traineddata and put it under tesseract/4.0.0.1/tessdata/

Usage

1
2
tesseract image.png image -l chi_sim
cat image.txt

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分享

JavaCV是什么

JavaCV 是一款开源的视觉处理库,基于GPLv2协议,对各种常用计算机视觉库封装后的一组jar包,封装了OpenCV、ffmpeg、videoInput…等计算机视觉编程人员常用库的接口。

maven引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<properties>
<javacpp.version>1.4.2</javacpp.version>
<!-- 这里要根据自己的平台选择不同的依赖 -->
<!--<javacpp.platform.dependencies>linux-x86_64</javacpp.platform.dependencies>-->
<javacpp.platform.dependencies>macosx-x86_64</javacpp.platform.dependencies>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacpp.version}</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv</artifactId>
<version>3.4.2-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
<classifier>${javacpp.platform.dependencies}</classifier>
</dependency>
</dependencies>

提取视频中的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 从视频中将每一帧的图片提取出来
*
* @param video
* @return
* @throws FrameGrabber.Exception
*/
public static List<BufferedImage> grab(File video) throws Exception {
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(video.getPath())) {
grabber.start();

List<BufferedImage> images = Lists.newArrayList();
Frame frame;
while ((frame = grabber.grabImage()) != null) {
images.add(Java2DFrameUtils.toBufferedImage(frame));
}
return images;
}
}

图片合成视频

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
private static class VideoRecorder implements Closeable {
private FFmpegFrameRecorder recorder;

public VideoRecorder(String output, int width, int height) throws FrameRecorder.Exception {
recorder = new FFmpegFrameRecorder(output, width, height);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setFrameRate(FPS);
recorder.setAudioBitrate(192000);
recorder.setSampleRate(44100);
recorder.setAudioChannels(2);
recorder.start();
}

public void addFrame(BufferedImage image) throws FrameRecorder.Exception {
Frame frame = Java2DFrameUtils.toFrame(image);
recorder.record(frame, avutil.AV_PIX_FMT_ARGB);
}

public void addAudio(File audioFile) throws FrameGrabber.Exception, FrameRecorder.Exception {
if (audioFile == null || !audioFile.exists()) {
return;
}
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(audioFile)) {
grabber.start();
Frame frame;
while ((frame = grabber.grabSamples()) != null) {
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}

@Override
public void close() throws IOException {
recorder.close();
}
}

Java图片处理工具类

这段代码是我四年前写的,当时的使用场景为使用tesseract做图片的预处理。功能包含图片二值化、移除杂色、横向切分、水平切分等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
import java.awt.Color;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;
import java.awt.image.PixelGrabber;
import java.util.ArrayList;
import java.util.List;

/**
* @author Gao Youbo
* @since 2014-05-29 14:34:13
*/
public class ImageUtils {

public static class SplitItem {

private int x;
private int w;
private int y;
private int h;

public int getX() {
return x;
}

public void setX(int x) {
this.x = x;
}

public int getW() {
return w;
}

public void setW(int w) {
this.w = w;
}

public int getY() {
return y;
}

public void setY(int y) {
this.y = y;
}

public int getH() {
return h;
}

public void setH(int h) {
this.h = h;
}

}


/**
* 图片纵向切分(切分为列)
*
* @param image
* @param minWidth 每个汉字的最小宽度,如果汉字的最小宽度小于该参数,那么认为系统将一个汉字截断了
* @return
*/
public static List<BufferedImage> splitLengthwaysWithMinWidth(BufferedImage image, int minWidth) {
if (minWidth < 0) {
minWidth = 0;
}
List<BufferedImage> subImgs = new ArrayList<>();
int width = image.getWidth();
int height = image.getHeight();
int startX = 0;
int endX = 0;
boolean start = false;
boolean end = false;
for (int x = 0; x < width; ++x) {
boolean blank = isXBlank(image, x);
if (!start) { //如果是白色
int space = spaceX(image, x);
x = x + space;
startX = x;
endX = x;
start = true;
}
if (start && !blank) {
endX = x;
}
int wordLength = endX - startX;
if (start && blank && wordLength > 0) {
// 汉字长度小于设定长度,那么认为这不是一个完成的汉字,而是将左右结构的汉字切分成了两份
if (wordLength < minWidth) {
int space = spaceX(image, x);
x = x + space;
} else {
end = true;
endX = x;
}
}
if (start && end && wordLength > 0) {
BufferedImage subImage = image.getSubimage(startX, 0, (endX - startX), height);
subImgs.add(subImage);
start = false;
end = false;
}
}
return subImgs;
}

/**
* x轴上的所有点是空白的(白色的)
*
* @param image
* @param x
* @return
*/
private static boolean isXBlank(BufferedImage image, int x) {
int height = image.getHeight();
for (int y = 0; y < height; y++) {
int rgb = image.getRGB(x, y);
if (isBlack(rgb)) {
return false;
}
}
return true;
}

/**
* 图片纵向切分(切分为列)
*
* @param image
* @param minGap 文字之间的最小间隙,如果间隙文字之间的间隙小于或等于该参数,那么认为该间隙为一个汉字上的正常间隙。主要处理左右结构的一些汉字,例如:”北、川、外...“
* @return
*/
public static List<BufferedImage> splitLengthways(BufferedImage image, int minGap) {
if (minGap < 0) {
minGap = 0;
}
List<BufferedImage> subImgs = new ArrayList<>();
int width = image.getWidth();
int height = image.getHeight();
List<Integer> weightlist = new ArrayList<>();
for (int x = 0; x < width; ++x) {
int count = 0;
for (int y = 0; y < height; ++y) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
}
if (minGap > 0) {
int space = spaceX(image, x);
if (space <= minGap) {
count = count + space;
}
}
weightlist.add(count);
}
List<SplitItem> splitItems = new ArrayList<>();
for (int i = 0; i < weightlist.size(); i++) {
int length = 0;
while (i < weightlist.size() && weightlist.get(i) > 0) {
i++;
length++;
}
if (length > 0) {
int x = i - length;
int w = length;
int y = 0;
int h = height;
SplitItem item = new SplitItem();
item.setX(x);
item.setW(w);
item.setY(y);
item.setH(h);
splitItems.add(item);
}
}
for (SplitItem splitItem : splitItems) {
subImgs.add(image.getSubimage(splitItem.getX(), splitItem.getY(), splitItem.getW(), splitItem.getH()));
}
return subImgs;
}

/**
* X轴上两个字之间的间距
*
* @param image
* @param currentX 当前索引所在的x坐标
* @return
*/
private static int spaceX(BufferedImage image, int currentX) {
int w = image.getWidth();
int h = image.getHeight();
int spaceLength = 0;
for (int x = currentX; x < w; x++) {
boolean space = true;
for (int y = 0; y < h; y++) {
if (isBlack(image.getRGB(x, y))) { //有黑色的,表明非空白
space = false;
break;
}
}
if (space) {
spaceLength++;
} else {
return spaceLength;
}
}
return spaceLength;
}


/**
* y轴上两个字之间的间距
*
* @param image
* @param currentY 当前索引所在的y坐标
* @return
*/
private static int spaceY(BufferedImage image, int currentY) {
int w = image.getWidth();
int h = image.getHeight();
int spaceLength = 0;
for (int y = currentY; y < h; y++) {
boolean space = true;
for (int x = 0; x < w; x++) {
if (isBlack(image.getRGB(x, y))) { //有黑色的,表明非空白
space = false;
break;
}
}
if (space) {
spaceLength++;
} else {
return spaceLength;
}
}
return spaceLength;
}


/**
* 图片横向切分(切分为行)
*
* @param image
* @param minGap 两行之间的最小间隙,如果间隙小于或等于该参数,那么认为没有折行
* @return
*/
public static List<BufferedImage> splitCrosswise(BufferedImage image, int minGap) {
if (minGap < 0) {
minGap = 0;
}
List<BufferedImage> subImgs = new ArrayList<>();
int w = image.getWidth();
int h = image.getHeight();
List<Integer> heightlist = new ArrayList<>();
for (int y = 0; y < h; y++) {
int count = 0;
for (int x = 0; x < w; x++) {
if (ImageUtils.isBlack(image.getRGB(x, y))) {
count++;
}
}
if (minGap > 0) {
int space = spaceY(image, y);
if (space <= minGap) {
count = count + space;
}
}
heightlist.add(count);
}
for (int i = 0; i < heightlist.size(); i++) {
int length = 0;
while (i < heightlist.size() && heightlist.get(i) > 0) {
i++;
length++;
}
if (length > 0) {
int y = i - length;
int x = 0;
int height = length;
int width = w;
BufferedImage bufferedImage = image.getSubimage(x, y, width, height);
subImgs.add(bufferedImage);
}
}
return subImgs;
}

/**
* 图片横向切分(切分为行)
*
* @param image
* @return
*/
public static List<BufferedImage> splitCrosswise(BufferedImage image) {
List<BufferedImage> subImgs = new ArrayList<>();
int w = image.getWidth();
int h = image.getHeight();
List<Integer> heightlist = new ArrayList<>();
for (int y = 0; y < h; y++) {
int count = 0;
for (int x = 0; x < w; x++) {
if (ImageUtils.isBlack(image.getRGB(x, y))) {
count++;
}
}
heightlist.add(count);
}
for (int i = 0; i < heightlist.size(); i++) {
int length = 0;
while (i < heightlist.size() && heightlist.get(i) > 0) {
i++;
length++;
}
if (length > 0) {
int y = i - length;
int x = 0;
int height = length;
int width = w;
BufferedImage bufferedImage = image.getSubimage(x, y, width, height);
subImgs.add(bufferedImage);
}
}
return subImgs;
}

/**
* 删除杂色(图片二值化)
* <p>
* 默认图片中字体颜色为黑色,如果非黑色像素全部替换为白色
*
* @param image
* @return
* @throws java.lang.InterruptedException
*/
public static final BufferedImage removeMotley(BufferedImage image) throws InterruptedException {
int width = image.getWidth();
int height = image.getHeight();
int[] pixels = new int[width * height];
int grey = 100;
PixelGrabber pixelGrabber = new PixelGrabber(image.getSource(), 0, 0, width, height, pixels, 0, width);
pixelGrabber.grabPixels();
ColorModel cm = ColorModel.getRGBdefault();
for (int i = 0; i < width * height; i++) {
int red, green, blue;
int alpha = cm.getAlpha(pixels[i]);
if (cm.getRed(pixels[i]) > grey) {
red = 255;
} else {
red = 0;
}
if (cm.getGreen(pixels[i]) > grey) {
green = 255;
} else {
green = 0;
}
if (cm.getBlue(pixels[i]) > grey) {
blue = 255;
} else {
blue = 0;
}
pixels[i] = alpha << 24 | red << 16 | green << 8 | blue; //通过移位重新构成某一点像素的RGB值
}
//将数组中的象素产生一个图像
Image tempImg = Toolkit.getDefaultToolkit().createImage(new MemoryImageSource(width, height, pixels, 0, width));
image = new BufferedImage(tempImg.getWidth(null), tempImg.getHeight(null), BufferedImage.TYPE_INT_BGR);
image.createGraphics().drawImage(tempImg, 0, 0, null);
return image;
}

/**
* 清除空白部分
*
* @param image
* @return
*/
public static BufferedImage removeSpace(BufferedImage image) {
BufferedImage result = removeTBWhite(image);
return removeLRWhite(result);
}

/**
* 移除上下白色部分(top bottom)
*
* @param image
* @return
*/
public static BufferedImage removeTBWhite(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int start = 0;
int end = 0;
Label1:
for (int y = 0; y < height; ++y) {
int count = 0;
for (int x = 0; x < width; ++x) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
start = y;
break Label1;
}
}
}
Label2:
for (int y = height - 1; y >= 0; --y) {
int count = 0;
for (int x = 0; x < width; ++x) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
end = y;
break Label2;
}
}
}
return image.getSubimage(0, start, width, end - start + 1);
}

/**
* 移除左右白色部分(left right)
*
* @param image
* @return
*/
public static BufferedImage removeLRWhite(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
int start = 0;
int end = 0;
Label1:
for (int x = 0; x < width; ++x) {
int count = 0;
for (int y = 0; y < height; ++y) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
start = x;
break Label1;
}
}
}
Label2:
for (int x = width - 1; x >= 0; --x) {
int count = 0;
for (int y = height - 1; y >= 0; --y) {
if (isBlack(image.getRGB(x, y))) {
count++;
}
if (count >= 1) {
end = x;
break Label2;
}
}
}
return image.getSubimage(start, 0, end - start + 1, height);
}

/**
* 移除黑色部分
*
* @param img
* @return
*/
public static BufferedImage removeBlack(BufferedImage img) {
int width = img.getWidth();
int height = img.getHeight();
int start = 0;
int end = 0;
Label1:
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
if (isBlack(img.getRGB(x, y))) {
start = y;
break Label1;
}
}
}
Label2:
for (int y = height - 1; y >= 0; --y) {
for (int x = 0; x < width; ++x) {
if (isBlack(img.getRGB(x, y))) {
end = y;
break Label2;
}
}
}
return img.getSubimage(0, start, width, end - start + 1);
}

/**
* 是否是黑色
*
* @param colorInt
* @return
*/
public static boolean isBlack(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue() <= 100;
}

/**
* 是否是白色
*
* @param colorInt
* @return
*/
public static boolean isWhite(int colorInt) {
Color color = new Color(colorInt);
return color.getRed() + color.getGreen() + color.getBlue() > 100;
}
}

小米手机安装Charles证书

平时使用Charles进行接口抓包,新换小米手机之后发现按照之前的流程安装Charles ssl证书不好使,百度了好久才找到一下解决办法。

  • 使用第三方浏览器(我用的是QQ浏览器)下载.pem 格式的文件
  • 将这个文件放入小米的Download文件夹下
  • 将.pem文件修改为.crt 格式
  • 设置—更多设置—系统安全—加密与凭据—从存储设备安装–选择Download文件夹下的文件
  • Finish~~

javacv使用笔记

使用过程中遇到的异常

异常:Could not initialize class org.bytedeco.javacpp.avutil

1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.NoClassDefFoundError: Could not initialize class org.bytedeco.javacpp.avutil
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:274)
at org.bytedeco.javacpp.Loader.load(Loader.java:385)
at org.bytedeco.javacpp.Loader.load(Loader.java:353)
at org.bytedeco.javacpp.avformat$AVFormatContext.<clinit>(avformat.java:2249)
at org.bytedeco.javacv.FFmpegFrameGrabber.startUnsafe(FFmpegFrameGrabber.java:346)
at org.bytedeco.javacv.FFmpegFrameGrabber.start(FFmpegFrameGrabber.java:340)

解决办法:

1
mvn package exec:java -Dplatform.dependencies -Dexec.mainClass=Demo

警告:Warning: data is not aligned! This can lead to a speedloss

出现这个警告是因为ffmpeg要求视频的宽度必须是32的倍数,高度必须是2的倍数,按要求修改下宽高就好了。

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import com.google.common.collect.Lists;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.bytedeco.javacpp.avcodec;
import org.bytedeco.javacpp.opencv_core;
import org.bytedeco.javacpp.opencv_imgcodecs;
import org.bytedeco.javacv.*;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Collections;
import java.util.List;

/**
* @author Gao Youbo
* @since 2018-08-15 16:43
*/
public class OpenCVUtils {
public static void main(String[] args) throws Exception {
List<BufferedImage> images = grab(new File("/data/opencv/test.mp4"));
int i = 1;
for (BufferedImage image : images) {
ImageIO.write(image, "jpg", new File("/data/opencv/frame/" + i + ".jpg"));
i++;
}

// grabAudioFromVideo(new File("/data/opencv/test.mp4"), new File("/data/opencv/test.aac"));

List<File> files = Lists.newArrayList(FileUtils.listFiles(new File("/data/opencv/frame/"), new String[]{"jpg"}, false));
Collections.sort(files, (o1, o2) -> {
int i1 = NumberUtils.toInt(StringUtils.substringBefore(o1.getName(), "."));
int i2 = NumberUtils.toInt(StringUtils.substringBefore(o2.getName(), "."));
return Integer.compare(i1, i2);
});
record("/data/opencv/out.mp4", files, new File("/data/opencv/test.aac"), 544, 960);
}

/**
* 将多个图片文件合成视频
*
* @param output 输出文件
* @param images 序列帧图片
* @param audioFile 音频
* @param width 宽
* @param height 高
* @throws Exception
*/
public static void record(String output, List<File> images, File audioFile, int width, int height) throws Exception {
try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(output, width, height);
FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(audioFile)) {
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setFrameRate(30);
recorder.setAudioBitrate(192000);
recorder.setAudioQuality(0);
recorder.setSampleRate(44100);
recorder.setAudioChannels(2);
recorder.start();

OpenCVFrameConverter.ToIplImage converter = new OpenCVFrameConverter.ToIplImage();
for (File file : images) {
opencv_core.IplImage image = opencv_imgcodecs.cvLoadImage(file.getPath());
recorder.record(converter.convert(image));
opencv_core.cvReleaseImage(image);
}

grabber.start();
Frame frame;
while ((frame = grabber.grabSamples()) != null) {
recorder.setTimestamp(frame.timestamp);
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}

/**
* 从视频中将每一帧的图片提取出来
*
* @param video
* @return
* @throws FrameGrabber.Exception
*/
public static List<BufferedImage> grab(File video) throws Exception {
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(video.getPath())) {
grabber.start();

List<BufferedImage> images = Lists.newArrayList();
Frame frame;
while ((frame = grabber.grabImage()) != null) {
images.add(Java2DFrameUtils.toBufferedImage(frame));
}
return images;
}
}

/**
* 从视频中提取出音频
*
* @param video
* @param outputAudio
*/
public static void grabAudioFromVideo(File video, File outputAudio) throws Exception {
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(video);
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputAudio, 1)) {
grabber.start();
recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
recorder.start();

Frame frame;
while ((frame = grabber.grab()) != null) {
if (frame.audioChannels == 1) {
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}
}

}

图片合成视频简单的封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private static class VideoRecorder implements Closeable {
private FFmpegFrameRecorder recorder;

public VideoRecorder(String output, int width, int height) throws FrameRecorder.Exception {
recorder = new FFmpegFrameRecorder(output, width, height);
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("mp4");
recorder.setFrameRate(FPS);
recorder.setAudioBitrate(192000);
recorder.setAudioQuality(0);
recorder.setSampleRate(44100);
recorder.setAudioChannels(2);
recorder.start();
}

public void addFrame(BufferedImage image) throws FrameRecorder.Exception {
Frame frame = Java2DFrameUtils.toFrame(image);
recorder.record(frame, avutil.AV_PIX_FMT_ARGB);
}

public void addAudio(File audioFile) throws FrameGrabber.Exception, FrameRecorder.Exception {
if (audioFile == null || !audioFile.exists()) {
return;
}
try (FFmpegFrameGrabber grabber = FFmpegFrameGrabber.createDefault(audioFile)) {
grabber.start();
Frame frame;
while ((frame = grabber.grabSamples()) != null) {
recorder.recordSamples(frame.sampleRate, frame.audioChannels, frame.samples);
}
}
}

@Override
public void close() throws IOException {
recorder.close();
}
}

解决maven打包时将不必要的包引入进来的问题

我在实际使用中只用到了ffmpeg,但是打包的时候却将flycapture、libdc1394、libfreenect、artoolkitplus、tesseract…等包都打进来了,这些都是我不需要的,下面贴出我的maven配置示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<properties>
<javacpp.version>1.4.2</javacpp.version>
<!-- 这里要根据自己的平台选择不同的依赖 -->
<!--<javacpp.platform.dependencies>linux-x86_64</javacpp.platform.dependencies>-->
<javacpp.platform.dependencies>macosx-x86_64</javacpp.platform.dependencies>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacpp.version}</version>
<exclusions>
<exclusion>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>opencv</artifactId>
<version>3.4.2-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco.javacpp-presets</groupId>
<artifactId>ffmpeg</artifactId>
<version>4.0.1-${javacpp.version}</version>
<classifier>${javacpp.platform.dependencies}</classifier>
</dependency>
</dependencies>

收藏两首程序员打油诗

1
2
3
4
5
6
7
8
商女不知亡国恨,一天到晚敲代码。
举头望明月,低头敲代码。
洛阳亲友如相问,就说我在敲代码。
少壮不努力,老大敲代码。
垂死病中惊坐起,今天还没敲代码。
生当作人杰,死亦敲代码。
人生自古谁无死,来生继续敲代码。
众里寻他千百度,蓦然回首,那人正在敲代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
写字楼里写字间,写字间里程序员;程序人员写程序,又拿程序换酒钱。
酒醒只在网上坐,酒醉还来网下眠;酒醉酒醒日复日,网上网下年复年。
宁愿老死程序间,只要老板多发钱;小车大房不去想,撰个二千好过年。
若要见识新世面,公务员比程序员;一个在天一在地,而且还比我们闲。
别人看我穿白领,我看别人穿名牌;天生我才写程序,臀大近视肩周炎。

年复一年春光度,度得他人做老板;老板扣我薄酒钱,没有酒钱怎过年。
春光逝去皱纹起,作起程序也委靡;来到水源把水灌,打死不做程序员。
别人笑我忒疯癫,我笑他人命太贱;状元三百六十行,偏偏来做程序员。
但愿老死电脑间,不愿鞠躬老板前;奔驰宝马贵者趣,公交自行程序员。
别人笑我忒疯癫,我笑自己命太贱;不见满街漂亮妹,哪个归得程序员。

不想只挣打工钱,那个老板愿发钱;小车大房咱要想,任我享用多悠闲。
比尔能搞个微软,我咋不能捞点钱;一个在天一在地,定有一日乾坤翻。
我在天来他在地,纵横天下山水间;傲视武林豪杰墓,一樽还垒风月山。
电脑面前眼发直,眼镜下面泪茫茫;做梦发财好几亿,从此不用手指忙。
哪知梦醒手空空,老板看到把我训;待到老时眼发花,走路不知哪是家。

小农村里小民房,小民房里小民工;小民工人写程序,又拿代码讨赏钱。
钱空只在代码中,钱醉仍在代码间;有钱无钱日复日,码上码下年复年。
但愿老死代码间,不愿鞠躬奥迪前,奥迪奔驰贵者趣,程序代码贫者缘。
若将贫贱比贫者,一在平地一在天;若将贫贱比车马,他得驱驰我得闲。
别人笑我忒疯癫,我笑他人看不穿;不见盖茨两手间,财权富贵世人鉴。

DelayQueue使用

DelayQueue特性

  • 队列中的元素都必须实现Delayed,元素可以指定延迟消费时长。
  • 实现了BlockingQueue接口,所以他是一个阻塞队列。
  • 本质上是基于PriorityQueue实现的。

贴一段我在实际生产环境中使用到代码

队列管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
* @author Gao Youbo
* @since 2018-07-26 19:53
*/
public class DelayQueueManager {
private static final Logger LOG = LoggerFactory.getLogger(DelayQueueManager.class);

private String name;
private ExecutorService executor;
private Thread monitorThread;
private DelayQueue<DelayTask<?>> delayQueue; // 延时队列

public DelayQueueManager(String name, int poolSize) {
this.name = name;
this.executor = Executors.newFixedThreadPool(poolSize);
this.delayQueue = new DelayQueue<>();
init();
}

/**
* 初始化
*/
private void init() {
monitorThread = new Thread(() -> {
execute();
}, "DelayQueueMonitor-" + name);
monitorThread.start();
}

private void execute() {
while (true) {
LOG.info("当前延时任务数量:" + delayQueue.size());
try {
// 从延时队列中获取任务
DelayTask<?> delayTask = delayQueue.take();
if (delayTask != null) {
Runnable task = delayTask.getTask();
if (task != null) {
// 提交到线程池执行task
executor.execute(task);
}
}
} catch (Exception e) {
LOG.error(null, e);
}
}
}

/**
* 添加任务
*
* @param id 任务编号
* @param task 任务
* @param time 延时时间
* @param unit 时间单位
*/
public void put(String id, Runnable task, long time, TimeUnit unit) {
long timeout = TimeUnit.MILLISECONDS.convert(time, unit);
long delayTimeMillis = System.currentTimeMillis() + timeout;
delayQueue.put(new DelayTask<>(id, delayTimeMillis, task));
}

/**
* 添加任务
*
* @param id 任务编号
* @param task 任务
* @param delayTimeMillis 延迟到什么时间点
*/
public void putAt(String id, Runnable task, long delayTimeMillis) {
delayQueue.put(new DelayTask<>(id, delayTimeMillis, task));
}

/**
* 根据任务编号删除任务
*
* @param id
* @return
*/
public boolean removeTaskById(String id) {
DelayTask task = new DelayTask(id, 0, null);
return delayQueue.remove(task);
}

/**
* 删除任务
*
* @param task
* @return
*/
public boolean removeTask(DelayTask task) {
return delayQueue.remove(task);
}
}

延迟任务对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

import java.util.Objects;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
* @author Gao Youbo
* @since 2018-07-26 19:54
*/
public class DelayTask<T extends Runnable> implements Delayed {
private final String id;
private final long delayTimeMillis; // 延迟到什么时间点执行
private final T task; // 任务

public DelayTask(String id, long delayTimeMillis, T task) {
this.id = id;
this.delayTimeMillis = delayTimeMillis;
this.task = task;
}

public T getTask() {
return task;
}

@Override
public int compareTo(Delayed o) {
DelayTask other = (DelayTask) o;
long diff = delayTimeMillis - other.delayTimeMillis;
if (diff > 0) {
return 1;
} else if (diff < 0) {
return -1;
} else {
return 0;
}
}

@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.delayTimeMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DelayTask<?> delayTask = (DelayTask<?>) o;
return Objects.equals(id, delayTask.id);
}

@Override
public int hashCode() {
return Objects.hash(id);
}
}

npm发布自己的包

  1. 如果没有账号,那么使用npm adduser去创建账号,中间会引导你输入用户名、密码、邮箱。
  2. 如果已经有账号,需要使用npm login去登录,同样会引导你输入正确的用户名、密码、邮箱。
  3. 登录成功之后使用npm publish将自己的包发不上去。

ffmpeg常用命令总结

音频格式转换

1
2
3
4
5
6
ffmpeg -y  -i aidemo.mp3  -acodec pcm_s16le -f s16le -ac 1 -ar 16000 16k.pcm 

// -acodec pcm_s16le pcm_s16le 16bits 编码器
// -f s16le 保存为16bits pcm格式
// -ac 1 单声道
// -ar 16000 16000采样率

查看音频格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
ffprobe -v quiet -print_format json -show_streams  aidemo.mp3

输出如下:
{
"streams": [
{
"index": 0,
"codec_name": "mp3", // mp3 格式
"codec_long_name": "MP3 (MPEG audio layer 3)",
"codec_type": "audio",
"codec_time_base": "1/16000",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"sample_fmt": "s16p",
"sample_rate": "16000", // 16000采样率
"channels": 1, // 单声道
"channel_layout": "mono",
"bits_per_sample": 0,
"r_frame_rate": "0/0",
"avg_frame_rate": "0/0",
"time_base": "1/14112000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 259096320,
"duration": "18.360000",
"bit_rate": "16000",
"disposition": {
"default": 0,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0
}
}
]
}

Guava Range使用方法

概念 表示范围 guava对应功能方法
(a..b) {x | a < x < b} open(C, C)
[a..b] {x | a <= x <= b} closed(C, C)
[a..b) {x | a <= x < b} closedOpen(C, C)
(a..b] {x | a < x <= b} openClosed(C, C)
(a..+∞) {x | x > a} greaterThan(C)
[a..+∞) {x | x >= a} atLeast(C)
(-∞..b) {x | x < b} lessThan(C)
(-∞..b] {x | x <= b} atMost(C)
(-∞..+∞) all values all()

微信红包的架构设计简介

转载自:http://colobu.com/2015/05/04/weixin-red-packets-design-discussion/

背景:有某个朋友在朋友圈咨询微信红包的架构,于是乎有了下面的文字(有误请提出,谢谢)
概况:2014年微信红包使用数据库硬抗整个流量,2015年使用cache抗流量。

微信的金额什么时候算?

答:微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储。
采取实时计算金额的考虑:预算需要占存储,实时效率很高,预算才效率低。

实时性:为什么明明抢到红包,点开后发现没有?

答:2014年的红包一点开就知道金额,分两次操作,先抢到金额,然后再转账。
2015年的红包的拆和抢是分离的,需要点两次,因此会出现抢到红包了,但点开后告知红包已经被领完的状况。进入到第一个页面不代表抢到,只表示当时红包还有。

分配:红包里的金额怎么算?为什么出现各个红包金额相差很大?

答:随机,额度在0.01和剩余平均值 * 2之间。 例如:发100块钱,总共10个红包,那么平均值是10块钱一个,那么发出来的红包的额度在0.01元~20元之间波动。
当前面3个红包总共被领了40块钱时,剩下60块钱,总共7个红包,那么这7个红包的额度在:0.01~(60/7 * 2)=17.14之间。
注意:这里的算法是每被抢一个后,剩下的会再次执行上面的这样的算法(Tim老师也觉得上述算法太复杂,不知基于什么样的考虑)。

这样算下去,会超过最开始的全部金额,因此到了最后面如果不够这么算,那么会采取如下算法:保证剩余用户能拿到最低1分钱即可。
如果前面的人手气不好,那么后面的余额越多,红包额度也就越多,因此实际概率一样的。

红包的设计

答:微信从财付通拉取金额数据过来,生成个数/红包类型/金额放到redis集群里,app端将红包ID的请求放入请求队列中,如果发现超过红包的个数,直接返回。根据红包的逻辑处理成功得到令牌请求,则由财付通进行一致性调用,通过像比特币一样,两边保存交易记录,交易后交给第三方服务审计,如果交易过程中出现不一致就强制回归。

并发性处理:红包如何计算被抢完?

答:cache会抵抗无效请求,将无效的请求过滤掉,实际进入到后台的量不大。cache记录红包个数,原子操作进行个数递减,到0表示被抢光。财付通按照20万笔每秒入账准备,但实际还不到8万每秒。

通如何保持8w每秒的写入?

答:多主sharding,水平扩展机器。

数据容量多少?

答:一个红包只占一条记录,有效期只有几天,因此不需要太多空间。

查询红包分配,压力大不?

答:抢到红包的人数和红包都在一条cache记录上,没有太大的查询压力。

一个红包一个队列?

答:没有队列,一个红包一条数据,数据上有一个计数器字段。

有没有从数据上证明每个红包的概率是不是均等?

答:不是绝对均等,就是一个简单的拍脑袋算法。

拍脑袋算法,会不会出现两个最佳?

答:会出现金额一样的,但是手气最佳只有一个,先抢到的那个最佳。

每领一个红包就更新数据么?

答:每抢到一个红包,就cas更新剩余金额和红包个数。

红包如何入库入账?

数据库会累加已经领取的个数与金额,插入一条领取记录。入账则是后台异步操作。

入帐出错怎么办?比如红包个数没了,但余额还有?

答:最后会有一个take all操作。另外还有一个对账来保障。

下面这张图是@周帆 同学的杰作!

命令行推送Jar包到nexus

1
mvn deploy:deploy-file -DgroupId=com.tencent -DartifactId=xinge -Dversion=1.1.8 -Dpackaging=jar -DrepositoryId=nexus -Dfile=/Users/gaoyoubo/xinge-push.jar -Durl=http://xxx.xxx.com:8081/nexus/content/repositories/thirdparty/ -DgeneratePom=false

并发队列-无界阻塞延迟队列DelayQueue原理探究

1
2
转载自:http://ifeve.com/%E5%B9%B6%E5%8F%91%E9%98%9F%E5%88%97-%E6%97%A0%E7%95%8C%E9%98%BB%E5%A1%9E%E5%BB%B6%E8%BF%9F%E9%98%9F%E5%88%97delayqueue%E5%8E%9F%E7%90%86%E6%8E%A2%E7%A9%B6/
最近在开发中正好有类似场景。

前言

DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。

DelayQueue类图结构

如图DelayQueue中内部使用的是PriorityQueue存放数据,使用ReentrantLock实现线程同步,可知是阻塞队列。另外队列里面的元素要实现Delayed接口,一个是获取当前剩余时间的接口,一个是元素比较的接口,因为这个是有优先级的队列。

offer操作

插入元素到队列,主要插入元素要实现Delayed接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {(2
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}

首先获取独占锁,然后添加元素到优先级队列,由于q是优先级队列,所以添加元素后,peek并不一定是当前添加的元素,如果(2)为true,说明当前元素e的优先级最小也就即将过期的,这时候激活avaliable变量条件队列里面的线程,通知他们队列里面有元素了。

take操作

获取并移除队列首元素,如果队列没有过期元素则等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//获取但不移除队首元素(1)
E first = q.peek();
if (first == null)
available.await();//(2)
else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay <= 0)//(3)
return q.poll();
else if (leader != null)//(4)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;//(5)
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)//(6)
available.signal();
lock.unlock();
}
}

第一次调用take时候由于队列空,所以调用(2)把当前线程放入available的条件队列等待,当执行offer并且添加的元素就是队首元素时候就会通知最先等待的线程激活,循环重新获取队首元素,这时候first假如不空,则调用getdelay方法看该元素海剩下多少时间就过期了,如果delay<=0则说明已经过期,则直接出队返回。否者看leader是否为null,不为null则说明是其他线程也在执行take则把该线程放入条件队列,否者是当前线程执行的take方法,则调用(5)await直到剩余过期时间到(这期间该线程会释放锁,所以其他线程可以offer添加元素,也可以take阻塞自己),剩余过期时间到后,该线程会重新竞争得到锁,重新进入循环。

(6)说明当前take返回了元素,如果当前队列还有元素则调用singal激活条件队列里面可能有的等待线程。leader那么为null,那么是第一次调用take获取过期元素的线程,第一次调用的线程调用设置等待时间的await方法等待数据过期,后面调用take的线程则调用await直到signal。

poll操作

获取并移除队头过期元素,否者返回null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
//如果队列为空,或者不为空但是队头元素没有过期则返回null
if (first == null || first.getDelay(TimeUnit.NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class DelayedEle implements Delayed {

private final long delayTime; //延迟时间
private final long expire; //到期时间
private String data; //数据

public DelayedEle(long delay, String data) {
delayTime = delay;
this.data = data;
expire = System.currentTimeMillis() + delay;
}

/**
* 剩余时间=到期时间-当前时间
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(this.expire - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
}

/**
* 优先队列里面优先级规则
*/
@Override
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("DelayedElement{");
sb.append("delay=").append(delayTime);
sb.append(", expire=").append(expire);
sb.append(", data='").append(data).append('\'');
sb.append('}');
return sb.toString();
}
}

public static void main(String[] args) {
DelayQueue<DelayedEle> delayQueue = new DelayQueue<DelayedEle>();

DelayedEle element1 = new DelayedEle(1000,"zlx");
DelayedEle element2 = new DelayedEle(1000,"gh");

delayQueue.offer(element1);
delayQueue.offer(element2);

element1 = delayQueue.take();
System.out.println(element1);
}

使用场景

TimerQueue的内部实现
ScheduledThreadPoolExecutor中DelayedWorkQueue是对其的优化使用

HexoClient 1.1.0版本发布

升级说明

  • 优化页面配色。
  • 优化文章预览、详情页面展示样式。
  • 文章内容修改后离开页面进行友好提示。
  • 支持hexo generatehexo deploy

下载地址

最新截图




Android studio terminal 中文乱码解决

Android studio terminal中文乱码,如下图:

解决办法打开~/.zshrc找到如下:

1
2
# You may need to manually set your language environment
# export LANG=en_US.UTF-8

export LANG=en_US.UTF-8 这一行解注。