Go 递归

递归,就是在运行的过程中调用自己。

通过以下阶乘函数来看下递归的写法:

func factorial(x uint) uint {
  if x == 0 {
    return 1
  }
  return x * factorial(x-1)
}

factorial 函数调用自己,形成函数递归,为了更好地理解这个函数是如何工作的,可以通过 factorial(2) 来理解。

  • x == 0 ? 不等于, x = 2

  • 计算 x - 1 的阶乘

    • x == 0 ? 不等于, x = 1

    • 计算 x - 1 的阶乘

      • x == 0 ? 等于, 返回 1
    • 返回 1 * 1

  • 返回 2 * 1

递归函数通过不断的调用自身完成需求,需要注意的是需要设置退出条件,否则就死循环了。

递归函数可以非常方便的解决数学上的问题,如阶乘,斐波那契数列等。

Go 闭包

在 Go 中可以在一个函数里创建另一个函数。如下:

func main() {
	add := func(x, y int) int {
		return x + y
	}

	fmt.Println(add(1,1))
}

add 是一个属性为 func(int, int) int (两个 int 类型参数,返回值类型为 int 的函数)的局部变量。这样的局部函数还可以访问其他局部变量。

func main() {
	x := 0
	increment := func() int {
		x++
		return x
	}

	fmt.Println(increment())
	fmt.Println(increment())
}

increment 函数为在 main 函数作用域中定义的变量 x 加1。 变量 x 可以被 increment 函数访问和修改。所以以上程序第一行将会输出:1,第二行将会输出:2。

这样的函数以及它引用的非本地变量称为闭包。 在本示例中,increment 函数和变量 x 形成闭包。

使用闭包的一种方法是编写一个函数,该函数返回另一个函数 - 当被调用时 - 可以生成一个数字序列。 例如,我们可以生成所有的偶数:

func makeEvenGenerator() func() uint {
	i := uint(0)
	return func() (ret uint) {
		ret = i
		i += 2
		return
	}
}

func main() {
	nextEven := makeEvenGenerator()
	fmt.Println(nextEven()) // 0
	fmt.Println(nextEven()) // 2
	fmt.Println(nextEven()) // 4
}

makeEvenGenerator 返回一个生成偶数的函数。 每次调用它时,它会将2添加到本地i变量中 - 与正常的局部变量不同,它会在调用之间保持不变。

原文

Closure

Dockerfile 构建参数

使用 Docker 做服务部署的时候,经常需要在构建的时候区分环境,让程序能够拿到环境变量,或者让程序能够针对不同环境做出不同的处理。

之前的写法比较原始,在本地打包 Docker 镜像后 push 到服务器,所以就可以在打包的时候修改环境变量的值。虽然只用到了一个区分开发,生产的变量,但是每次都这么做还是比较烦。

最近重新整理 Dockerfile , 又看了下文档,发现可以在 docker build 阶段传入参数的。

如下所示:

FROM golang

ARG app_env

ENV APP_ENV $app_env

...

build 时,可以这样写

docker build -t app -f Dockerfile . --build-arg app_env=dev

这样就可以在 go 程序里 通过 os.Getenv(“APP_ENV”) 拿到环境变量信息,进行不同处理了。

参考资料:

ARG 构建参数

Golang and Docker for development and production

迁移到 GitHub pages 小记

在 Vultr vps 挂了N个月之后,无奈只能选择把这堆东西迁到 GitHub pages 了,看起来是唯一能选的比较不错的选择。虽然很早以前也看过一些迁到 GitHub pages 的教程,但是实施起来还是有些新的收获。

开始前没注意看文档,其实创建 repo 的时候对于 usrename 的项目,repo 名必须是 username.github.io。

如果不想使用 username.github.io 作为域名的话,要在 repo->settings->GitHub Pages 中,设置一个 Custom domain,然后 Save。

GitHub 默认是提供了使用 jekyll 作为 GitHub pages 的内容处理程序。而如果并不想使用 jekyll 的话,也可以。比如我就是使用 hexo ,把 generate 的内容作为 git 内容提交了的。

但是还有一个要注意的是,把静态内容提交到 repo 后,需要过一会才会生效。所以刚提交完时,会返回 404 ,不要着急,过一会就好了。

另外目前 GitHub 还提供了使用自定义域名时启用 HTTPS 的选项,启用后就会强制使用 HTTPS 打开网站了。

Nginx HTTP/2 Server Push

Nginx 在最新的 1.13.9 版本中,增加了对 HTTP/2 Server Push 的支持,以下就简单介绍下如何使用。

以下内容主要来自 Introducing HTTP/2 Server Push with NGINX 1.13.9,进行了简单的整理。

推送指定资源

首先,在 Nginx 中进行配置:

server {
    #开启HTTP/2
    listen 443 ssl http2;

    ssl_certificate ssl/certificate.pem;
    ssl_certificate_key ssl/key.pem;

    root /var/www/html;

    # 当请求 demo.html 时,推送 /style.css, /image1.jpg, /image2.jpg
    location = /demo.html {
        http2_push /style.css;
        http2_push /image1.jpg;
        http2_push /image2.jpg;
    }
}

可以通过 Chrome Developer Tools 查看效果。在 Network 中,可以看到 demo.html 的请求和推送到客户端的内容。

如下图所示,可以看到 style.css,image1.jpg,image2.jpg。它们都是 demo.html 请求的一部分。

自动推送

以上是在页面加载时推送指定资源的例子,但是很多情况下,并不能指定要推送哪些资源,因此,Nginx 还支持通过 http2_push_preload 指令,自动分析响应头中的 Link header,来自动推送这些资源。

server {
    #开启HTTP/2
    listen 443 ssl http2;

    ssl_certificate ssl/certificate.pem;
    ssl_certificate_key ssl/key.pem;

    root /var/www/html;

    location = /myapp {
        proxy_pass http://upstream;
        http2_push_preload on;
    }
}

通过如上的配置,当 upstream 返回的响应头中包含 Link header 时,

Link: </style.css>; as=style; rel=preload

Nginx 即会开启一个推送, 内容则是 /style.css ,Link header 的路径必须是绝对路径。

如果想要推送多个资源,可以添加多个 Link header , 或更直接一些,把所有资源添加到一个 Link header 中。

Link: </style.css>; as=style; rel=preload, </favicon.ico>; as=image; rel=preload

如果不想让 Nginx 推送某个资源,为该 header 添加一个 nopush 参数即可。

Link: </nginx.png>; as=image; rel=preload; nopush

有选择的推送

由于 HTTP/2 规范并没有解决是否要推送哪些资源的问题,但是比较合理的方式是知道需要推送哪些资源,并且客户端没有缓存过这些资源,才进行推送。

所以 Nginx 支持了添加条件,只在符合条件时才进行推送

当客户端请求时包含了 cookie ,Nginx 将只会推送一次资源。

server {
    listen 443 ssl http2 default_server;

    ssl_certificate ssl/certificate.pem;
    ssl_certificate_key ssl/key.pem;


    root /var/www/html;
    http2_push_preload on;

    location = /demo.html {
        add_header Set-Cookie "session=1";
        add_header Link $resources;
    }
}


map $http_cookie $resources {
    "~*session=1" "";
    default "</style.css>; as=style; rel=preload, </image1.jpg>; as=image; rel=preload,
             </image2.jpg>; as=style; rel=preload";
}

相关链接:

在Go中使用 HTTP/2 Server Push

参考资料:

Server Push (HTTP/2)

Go slice,struct排序

Go中有时会需要对slice,或多个struct进行排序,其实很简单。

slice

对于 slice 的排序,可以直接使用 sort 包提供的方法,

int

s := []int{3,2,4,1}
sort.Ints(s)
fmt.Println(s) // [1,2,3,4]

string

s := []string{"Go", "Bravo", "Gopher", "Alpha", "Grin", "Delta"}
sort.Strings(s)
fmt.Println(s) // [Alpha Bravo Delta Go Gopher Grin]

float

s := []float64{5.2, -1.3, 0.7, -3.8, 2.6} 
sort.Float64s(s)
fmt.Println(s) // [-3.8,-1.3,0.7,2.6,5.2]

struct

以上都是 sort 默认提供的方法,但是对于 struct ,就需要自己实现 sort.Interface。

type Person struct {
	Name string
	Age int
}

type byAge []Person

func (a ByAge) Len() int           { return len(a) }
func (a ByAge) Less(i, j int) bool { return a[i].Age < a[j].Age }
func (a ByAge) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }

func main() {
    family := []Person{
        {"Alice", 23},
        {"Eve", 2},
        {"Bob", 25},
    }
    sort.Sort(ByAge(family))
    fmt.Println(family) // [{Eve 2} {Alice 23} {Bob 25}]
}

Go struct 转 map 使用自定义标签

今天工作遇到一个问题,之前将 struct 转 map 的时候,没有注意 field 大小写的问题,具体的说,是没有注意 field name 与实际需要的 name 的区别,其实就是需要自定义转为 map 之后的name,今天发现问题后,看了下引用包的源码,发现是可以自定义标签的,就跟 struct 转 JSON 一样。

代码也很简单,加上 `structs:"name"` 即可。

package main

import (
	"fmt"
	"github.com/fatih/structs"
)

type Server struct {
	Name string `structs:"server_name"`
	ID   int    `structs:"server_id"`
}

func main() {
	server := &Server{
		Name: "gopher",
		ID:   123456,
	}

	fmt.Printf("struct : %v\n", server) //struct : &{gopher 123456}

	serverMap := structs.Map(server)

	fmt.Printf("map : %v\n", serverMap) //map : map[server_name:gopher server_id:123456]

这样就可以拿到自定义key的map了。

Go 检测文件内容类型

有时候需要检测文件的内容类型或 MIME 类型,为此,需要打开文件并读取前512个字节(因为DetectContentType()函数值使用前512个字节),所以不需要读取更多内容。这个函数会返回一个 MIME 类型,如 application/json 或 image/jpeg。

package main

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

func main() {
	fileArr := []string{"file/1.pdf", "file/2.jpg", "file/3.docx", "file/4.xml", "file/5.azw3", "file/6.zip", "file/7.torrent"}

	for _, fileName := range fileArr {

		f, err := os.Open(fileName)
		if err != nil {
			panic(err)
		}
		defer f.Close()

		contentType, err := getFileContentType(f)
		if err != nil {
			panic(err)
		}

		fmt.Print(fmt.Sprintf("File Name : %v, Content Type : %s\n", fileName, contentType))
	}
}

func getFileContentType(out *os.File) (string, error) {
	buffer := make([]byte, 512)

	_, err := out.Read(buffer)
	if err != nil {
		return "", err
	}

	contentType := http.DetectContentType(buffer)

	return contentType, nil
}

Docker多阶段构建

Docker在17.05引入了多阶段构建的功能,就是将之前需要多次运行build的Dockerfile,现在可以写到一个里面,只build一次,就可以达到同样的效果。

另外,实际应用中,还可以通过这样,非常简单的将最后可执行文件放入极小的镜像中使用。

比如之前说过的,做一个Beego的Docker镜像,如果使用官方的镜像运行,占用空间稍微有点大了,虽然比起Ubuntu,CentOS动辄5-600M还好一些,但是再跟只有10几M,甚至几M的相比,还是太大了。

下面通过一个Dockerfile来了解一下:

#指定构建镜像
FROM golang:1.9.2 as builder

#指定工作目录
WORKDIR /go/src/app
#将当前项目文件copy到够姜镜像的工作目录中
COPY . .

#在构建镜像中执行go build,这里指定了构建的目标平台,具体的构建命令针对具体情况修改即可
#也可简单的 go build 即可
#另外需要注意依赖包的问题
RUN CGO_ENABLED=0 GOOS=linux go build -x -v -ldflags '-w -s' -a -o app .

#使用alpine作为运行的镜像
FROM alpine:latest

#指定工作目录
WORKDIR /go/src/app

#如果程序中涉及到需要连接DB,并且需要指定时区,需要copy时区文件,为了省事,直接从构建镜像中复制了,可以正常使用
COPY --from=builder /usr/local/go/lib/time/zoneinfo.zip /usr/local/go/lib/time/zoneinfo.zip
#将构建镜像中build完成的可执行文件copy到工作目录
COPY --from=builder /go/src/app/app ./app

EXPOSE 8080

CMD ["./app"]

以上就是使用多阶段构建的Dockerfile了,非常明了,只需要docker build就可以了。

然而目前公司的测试环境还没升级到17.05,并不能使用此功能。

Go语言使用redigo操作GEO(3) - 获取范围集合

上一篇记录了使用redigo操作Redis的获取两点距离的功能。

今天继续来写一下,获取一个坐标点指定距离范围内地理位置的集合。

完成此功能,需要使用Redis的 GEORADIUS 命令。

语法:

GEORADIUS key longitude latitude radius [m|km|ft|mi] [WITHCOORD] [WITHDIST] [ASC|DESC] [WITHHASH] [COUNT count]

其中,longitude latitude 表示地理位置的坐标,radius表示范围的距离,单位可以是m,km,ft,mi,依次为米,千米,英里,英尺。

后边的可选参数中:

  • WITHCOORD:传入WITHCOORD参数,返回结果会带上匹配位置的经纬度。
  • WITHDIST:传入WITHDIST参数,返回结果会带上匹配位置与给定地理位置的距离,距离的单位与 GEORADIUS 命令传入的单位一致。
  • ASC|DESC:默认结果是未排序的,ASC表示从近到远排序,DESC表示从远到近排序。
  • WITHHASH:传入WITHHASH参数,返回结果会带上匹配位置的hash值。hash为52位有符号整数。
  • COUNT count:传入COUNT参数,返回指定数量的结果。

参照文档,可以发现,默认情况下 GEORADIUS 会返回全部匹配的元素,而传入 COUNT 参数会截取指定的部分元素返回,但是由于此命令还需要对返回结果进行排序,所以如果元素较多的情况下,即使使用了 COUNT 参数,查找速度也会很慢。所以此参数仅适用于减小带宽占用,一次性返回少数数据,多次查询。

GEORADIUS 的返回值是一个数组:

  • 如果传入了WITHCOORD,WITHDIST,WITHHASH参数,会返回二维的数组,其中每个子数组表示一个元素
  • 如果没有传入上述参数,会返回一个一维的数组,值为元素名,如[“tianjin”,“baoding”]

如果返回的是二维数组,子数组的第一个元素是对应位置的名字,其他的会根据传入的参数当做数组的元素返回,其中:

  • 距离依然是一个双精度浮点数,单位与传入的单位参数一致。
  • GEOHASH 是一个整数。
  • 坐标分别为经度,纬度,其中经度在前。

如:

 GEORADIUS citylist 116.280316 39.9329 200 km WITHCOORD

会返回

1) 1) "beijing"
   2) 1) "116.40528291463851929"
      2) "39.9049884229125027"
2) 1) "baoding"
   2) 1) "115.33530682325363159"
      2) "38.87121760640306434"
3) 1) "tangshan"
   2) 1) "116.94219142198562622"
      2) "39.05078232277295314"
4) 1) "tianjin"
   2) 1) "117.0153459906578064"
      2) "39.12522961794389431"

那么,使用redigo如何实现呢?

有WITHCOORD,WITHDIST,WITHHASH参数

暂时还没想到怎么解决这个比较好。

无WITHCOORD,WITHDIST,WITHHASH参数

func radius(key string, lng, lat float64, radius int, unit string) []string {
	rc := RedisClient.Get()
	defer rc.Close()

	pos, _ := redis.Strings(rc.Do("GEORADIUS", key, lng, lat, radius, unit))
	return pos
}

该方法则会返回符合条件的元素的名称,如:

[beijing baoding tangshan tianjin]