Go语言使用redigo操作GEO(2) - 获取两点距离

上一篇简单记录了在Go中使用redigo这个包操作Redis,简单的使用了添加和查询。

今天来试一下计算距离。之前也写到过在MySQL中计算坐标的距离,及排序

在上一篇中疏忽了一个问题,使用的几个坐标点是用高德地图获取到的,但是Redis中GEO是使用了 WGS84 坐标系的。实际使用中需要注意一下,这里有一篇文章详细介绍了不同坐标系的区别。

添加测试数据

先添加几条测试用的数据。

push("citylist", "tianjin", 39.1252291, 117.0153461)
push("citylist", "tangshan", 39.0507819, 116.9421939)
push("citylist", "baoding", 38.8712164, 115.3353061)

添加了三个城市。

查询距离

使用GEODIST方法查询两点的距离。手册见此

语法: GEODIST key member1 member2 [unit]

第四个参数可选,可选值为:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

默认是米。

简单写一个方法,传入key和要查询的两点,及距离单位。

func dist(key, m1, m2, unit string) float64 {
	rc := RedisClient.Get()
	defer rc.Close()

	rs1, _ := redis.Float64(rc.Do("GEODIST", key, m1, m2, unit))

	return rs
}

GEODIST返回值是双精度浮点数,这里直接使用redigo包的Float64方法转换返回了。

使用

rs1 := dist("citylist", "beijing", "tianjin", "km")
rs2 := dist("citylist", "beijing", "tangshan", "km")
rs3 := dist("citylist", "beijing", "baoding", "km")

fmt.Println("北京-天津", rs1)
fmt.Println("北京-唐山", rs2)
fmt.Println("北京-保定", rs3)

Go语言使用redigo操作GEO(1) - 添加和查询

周五的时候群里有人遇到在Go中使用Redis GEO的问题,顺手搜了下解决办法。发现还挺简单的,周末无事,写下来记一下。

GEO是在Redis 3.2加入的功能,手册见此

本示例中只演示GEOADD和GEOPOS功能。

redigo这个客户端还挺好用的,但是个人觉得略有不足的是文档不全(指的是中文的文档),但是好在可以看源码解决一些不太清楚的问题。

0.连接Redis

首先习惯性的创建了连接池,嗯,连接池。

func init() {
	RedisClient = &redis.Pool{
		MaxIdle:   MaxIdle,
		MaxActive: MaxActive,

		IdleTimeout: 60 * time.Second,

		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", RedisHost)

			if err != nil {
				return nil, err
			}

			/*if _, err := c.Do("AUTH", RedisPwd); err != nil {
				c.Close()
				return nil, err
			}*/
			c.Do("SELECT", RedisDb)

			return c, nil
		},
	}
}

1.添加值

为了简单起见,创建了一个方法用于向传入的key中写入name的坐标点。

直接返回错误。

func push(key, name string, lat, lng float64) error {
	rc := RedisClient.Get()
	defer rc.Close()

	_, err := rc.Do("GEOADD", key, lng, lat, name)
	return err
}

2.取回值

同样,创建一个方法,在Redis中取回key中name的坐标点的值。

但是注意,这里在执行完 GEOPOS 后,调用 redigo 包中的 Positions 方法把返回结果转成 float64 的数组。

然后返回这个数组和错误

func get(key, name string) ([]*[2]float64, error) {
	rc := RedisClient.Get()
	defer rc.Close()

	res, err := redis.Positions(rc.Do("GEOPOS", key, name))

	return res, err
}

3.main方法中的简单代码

func main() {
	key := "citylist"
	name := "beijing"
	lat := 39.9329
	lng := 116.280316
	err := push(key, name, lat, lng)
	if err != nil {
		panic(err)
	}
	fmt.Println("坐标写入完成")
	fmt.Println("获取刚刚写入的值")

	res, err := get(key, name)
	if err != nil {
		panic(err)
	}
	fmt.Println(res[0][0], res[0][1])
}

就这么简单。

import部分和定义的几个全局变量就不用写了,

LeetCode 561.Array Partition I(数组分区 1) - Go实现

题目地址:561. Array Partition I

题目描述:

Given an array of 2n integers, your task is to group these integers into n pairs of integer, say (a1, b1), (a2, b2), …, (an, bn) which makes sum of min(ai, bi) for all i from 1 to n as large as possible.

Example 1:
Input: [1,4,3,2]

Output: 4

Explanation: n is 2, and the maximum sum of pairs is 4 = min(1, 2) + min(3, 4).
Note:

​ n is a positive integer, which is in the range of [1, 10000]. ​ All the integers in the array will be in the range of [-10000, 10000].

题目大意:

给定一个长度为2n的数组,要把它分成n个分组,即每组有两个数,返回每组中最小值的总和,使和最大。

理解了大意就知道思路了,又看了下论坛里的算法分析

解决方案基本就是先按从小到大排序,这样相邻的数字是最接近的,然后再分成两两一组,取每组中的第一个数相加即可。

package main

import (
	"fmt"
	"sort"
)

func main() {
	nums := []int{4, 5, 6, 1}
	n := arrayPairSum(nums)
	fmt.Println(n)
}

func arrayPairSum(nums []int) int {
	sort.Ints(nums)
	sum := 0
	length := len(nums)
	for i := 0; i < length; i += 2 {
		sum += nums[i]
	}
	return sum
}

在线查看结果:The Go Playground

其实这里主要用到了Go的sort包给int数组排序。排序后遍历数组,每次递增2就可以了。

【译】使用Go和Angular通过WebSocket构建实时聊天应用

写在前面

本文原文,详细讲解了如何使用Go和Angular通过WebSocket构建实时聊天应用。

正文

我最近听到很多关于WebSocket的东西,以及WebSocket如何在应用程序和服务器之间实现实时通信。WebSocket作为RESTful API的替代和补充,已经存在了很长时间。使用WebSocket可以做例如实时聊天,与IoT通信,游戏,和其他很多需要在客户端和服务器之间进行即时消息传递的东西。

最近一段时间,我使用了一个叫Socket.io的库,用来在Node.js中使用websockets,但是当我真正使用Go以后,我打算研究一下如何在Go中使用WebSocket。

通过本文,我们将学习如何创建一个聊天应用,其中客户端是一个 Angular 2 应用,服务端使用Go。

要求

在这个应用中有很多操作,所以有一些必要的前提条件,如下所示:

处理所有消息和客户端的聊天服务器使用Go编写。客户端前端使用 Angular 2编写,has a dependency of the Node Package Manager (NPM) which ships with Node.js.

创建Go聊天服务器

我们打算先开发整个应用的服务器端部分,它需要依赖几个第三方的包。

在命令行执行以下命令,下载第三方包:

//Install Go Dependencies
go get github.com/gorilla/websocket
go get github.com/satori/go.uuid

websocket包的作者同时也是 Mux 这个路由包 的作者,我们还需要一个UUID包来分配每一个客户端的唯一ID。

在 $GOPATH 目录创建一个新的项目,我自己的项目目录是 $GOPATH/src/github.com/nraboy/realtime-chat/main.go。

在进行下一步之前,需要注意的是,我从 Dinosaurs CodeGorilla websocket chat example 获取了一部分Go 代码,为了避免剽窃的嫌疑,我使用了很多原始代码中的一部分,但我也为这个项目加入了很多自己的独特的东西。

这次我们要做的聊天应用有3个结构体:

// $GOPATH/src/github.com/nraboy/realtime-chat/main.go
type ClientManager struct {
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
}
 
type Client struct {
    id     string
    socket *websocket.Conn
    send   chan []byte
}
 
type Message struct {
    Sender    string `json:"sender,omitempty"`
    Recipient string `json:"recipient,omitempty"`
    Content   string `json:"content,omitempty"`
}

ClientManager用于管理所有已连接的客户端,尝试连接的客户端,已经断开连接等待删除的客户端,和所有已连接客户端收发的消息。

每个客户端有一个唯一的ID,一个socket连接,和等待发送的消息。

为了增加传递的数据的复杂性,消息将使用 JSON 格式。而不是传递一串不容易被理解,阅读的数据。使用JSON格式,我们可以使用元数据和其他有用的东西。每一条消息将包含发送消息的客户端,接收消息的客户端,和消息的实际内容。

首先定义一个全局的ClientManager。

//$GOPATH/src/github.com/nraboy/realtime-chat/main.go
var manager = ClientManager{
    broadcast:  make(chan []byte),
    register:   make(chan *Client),
    unregister: make(chan *Client),
    clients:    make(map[*Client]bool),
}

服务器端将使用3个goroutine,一个用于管理客户端,一个用于读取websocket数据,另一个用于往websocket里写数据。这里指的是读取和写入的goroutine将为每个连接的客户端创建一个新的实例。所有的goroutine将循环运行直至不再需要。

编写如下代码,来开始服务:

//$GOPATH/src/github.com/nraboy/realtime-chat/main.go
func (manager *ClientManager) start() {
    for {
        select {
        case conn := <-manager.register:
        	manager.clients[conn] = true
        	jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected."})
            manager.send(jsonMessage, conn)
        case conn := <-manager.unregister:
            if _, ok := manager.clients[conn]; ok {
                close(conn.send)
                delete(manager.clients, conn)
                jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected."})
                manager.send(jsonMessage, conn)
            }
        case message := <-manager.broadcast:
            for conn := range manager.clients {
                select {
                case conn.send <- message:
                default:
                    close(conn.send)
                    delete(manager.clients, conn)
                }
            }
        }
    }
}

每当 manager.register 接收到数据,这个正在建立连接的客户端将会被添加到 manager (前文创建的 ClientManager 实例)的 clients 中。然后,将向所有其他客户端发送一条JSON消息。

同时,如果客户端断开连接,manager.unregister channel将会收到消息,断开连接的客户端的 channel 中的数据将被关闭,客户端也会从manager中删除。然后发送消息给其他的客户端告知某个客户端已断开连接。

如果 manager.broadcast channel 中存在数据,则表示正在尝试发送和接收消息。我们打算遍历每个已连接的客户端,将消息发送给它们。如果由于某些原因,channel 被阻塞或消息无法发送,我们会认为这个客户端已断开连接,然后将其删除。

为了使代码简洁,创建一个 manager.send 方法遍历每个客户端。

//$GOPATH/src/github.com/nraboy/realtime-chat/main.go
func (manager *ClientManager) send(message []byte, ignore *Client) {
    for conn := range manager.clients {
        if conn != ignore {
            conn.send <- message
        }
    }
}

至于conn.send如何发送数据,会在后面探讨。

现在我们可以探索 goroutine 如何读取客户端发送的 websocket 数据。这个 goroutine 的关键是读取 socket 数据,并将数据添加到 manager.boradcast 做进一步处理。

//$GOPATH/src/github.com/nraboy/realtime-chat/main.go
func (c *Client) read() {
    defer func() {
        manager.unregister <- c
        c.socket.Close()
    }()
 
    for {
        _, message, err := c.socket.ReadMessage()
        if err != nil {
            manager.unregister <- c
            c.socket.Close()
            break
        }
        jsonMessage, _ := json.Marshal(&Message{Sender: c.id, Content: string(message)})
        manager.broadcast <- jsonMessage
    }
}

如果读取 websocket 数据出错,可能意味着客户端已经断开连接。如果是这样,我们需要从服务器中注销这个客户端。

还记得前边的 conn.send 吗,它用来在第三个 goroutine 中写数据。

//$GOPATH/src/github.com/nraboy/realtime-chat/main.go
func (c *Client) write() {
    defer func() {
        c.socket.Close()
    }()
 
    for {
        select {
        case message, ok := <-c.send:
            if !ok {
                c.socket.WriteMessage(websocket.CloseMessage, []byte{})
                return
            }
 
            c.socket.WriteMessage(websocket.TextMessage, message)
        }
    }
}

如果 c.send channel有数据,我们将尝试发送这些数据。如果由于某些原因,channel 运行不正常,我们将向客户端发送断开连接的消息。

那么,如何启动这些 goroutine 呢,当我们启动服务器时,服务器 goroutine 将会启动,当有客户端连接时,其他 goroutine 将会启动。

main方法中的代码:

//$GOPATH/src/github.com/nraboy/realtime-chat/main.goGo
func main() {
    fmt.Println("Starting application...")
    go manager.start()
    http.HandleFunc("/ws", wsPage)
    http.ListenAndServe(":12345", nil)
}

我们在12345端口启动服务器,通过 websocket 连接访问。名为 wsPage 的方法如下所示:

//$GOPATH/src/github.com/nraboy/realtime-chat/main.goGo
func wsPage(res http.ResponseWriter, req *http.Request) {
    conn, error := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(res, req, nil)
    if error != nil {
        http.NotFound(res, req)
        return
    }
    client := &Client{id: uuid.NewV4().String(), socket: conn, send: make(chan []byte)}
 
    manager.register <- client
 
    go client.read()
    go client.write()
}

通过使用 websocket 包将HTTP请求升级到websocket请求。通过添加 CheckOrigin ,我们可以接受来自外部域的请求,从而消除跨域资源共享(CORS)的错误。

创建连接后,将创建一个客户端,并分配唯一的ID。如前所述,该客户端已经注册到服务器。客户端注册后,读写 goroutine 将被触发。

此时,我们可以通过如下命令启动应用。

//Run Go Application
go run *.go

你不能在直接 web 浏览器中测试,但是可以建立一个 websocket 连接到 ws://localhost:12345/ws。

创建Angular2 聊天客户端

现在我们需要创建一个客户端的应用,客户端可以发送和接收消息。假设您已经安装了Angular 2 CLI,请执行以下操作:

//Create New Angular 2 Project
ng new SocketExample

执行完将会生成一个单页应用,而我们想要完成的内容,是下方的动图演示的这样。

补充:此处需cd SocketExmapl && npm install。

JavaScript的 websocket 在Angular 2提供的一个类中。使用 Angular 2 CLI,通过执行如下操作创建provider。

//Create Angular 2 Provider
ng g service socket

上述命令会在您的项目中创建 **src/app/socket.service.ts ** 和 src/app/socket.service.spec.ts 。spec文件用于单元测试,不在本文讨论范围内。打开 src/app/socket.service.ts 文件,编写以下 TypeScript 代码:

//src/app/socket.service.ts
import { Injectable, EventEmitter } from '@angular/core';
 
@Injectable()
export class SocketService {
 
    private socket: WebSocket;
    private listener: EventEmitter<any> = new EventEmitter();
 
    public constructor() {
        this.socket = new WebSocket("ws://localhost:12345/ws");
        this.socket.onopen = event => {
            this.listener.emit({"type": "open", "data": event});
        }
        this.socket.onclose = event => {
            this.listener.emit({"type": "close", "data": event});
        }
        this.socket.onmessage = event => {
            this.listener.emit({"type": "message", "data": JSON.parse(event.data)});
        }
    }
 
    public send(data: string) {
        this.socket.send(data);
    }
 
    public close() {
        this.socket.close();
    }
 
    public getEventListener() {
        return this.listener;
    }
 
}

该提供者是可以注射的,并在触发某些事件事发送数据。在构造方法中,建立了与Go应用的WebSocket 连接,并创建了3个事件监听器。分别对应每个socket创建和销毁时,及接收到消息时。

send方法允许我们向Go应用发送消息,close方法用于通知Go应用我们将断开连接。

提供者程序已创建,但是还不能在我们的的应用程序的任何文件中使用。因此,我们需要将其添加到 src/app/app.module.ts 文件的 @NgModule 块中。打开文件并输入:

//src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
 
import { AppComponent } from './app.component';
import { SocketService } from "./socket.service";
 
@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        FormsModule,
        HttpModule
    ],
    providers: [SocketService],
    bootstrap: [AppComponent]
})
export class AppModule { }

需要注意的是,此时我们已经将provider导入并且添加到 @NgModule 块的 providers数组中了。

现在我们可以专注处理页面的逻辑了。打开 src/app/app.component.ts 文件,并输入以下代码:

//src/app/app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { SocketService } from "./socket.service";
 
@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
 
    public messages: Array<any>;
    public chatBox: string;
 
    public constructor(private socket: SocketService) {
        this.messages = [];
        this.chatBox = "";
    }
 
    public ngOnInit() {
        this.socket.getEventListener().subscribe(event => {
            if(event.type == "message") {
                let data = event.data.content;
                if(event.data.sender) {
                    data = event.data.sender + ": " + data;
                }
                this.messages.push(data);
            }
            if(event.type == "close") {
                this.messages.push("/The socket connection has been closed");
            }
            if(event.type == "open") {
                this.messages.push("/The socket connection has been established");
            }
        });
    }
 
    public ngOnDestroy() {
        this.socket.close();
    }
 
    public send() {
        if(this.chatBox) {
            this.socket.send(this.chatBox);
            this.chatBox = "";
        }
    }
 
    public isSystemMessage(message: string) {
        return message.startsWith("/") ? "<strong>" + message.substring(1) + "</strong>" : message;
    }
 
}

在上述 AppComponent类的构造方法中,我们注册服务提供者并初始化需要绑定到UI的变量。在构造函数中加载或订阅不太好,我们使用ngOninit方法来代替。

//src/app/app.component.ts
public ngOnInit() {
    this.socket.getEventListener().subscribe(event => {
        if(event.type == "message") {
            let data = event.data.content;
            if(event.data.sender) {
                data = event.data.sender + ": " + data;
            }
            this.messages.push(data);
        }
        if(event.type == "close") {
            this.messages.push("/The socket connection has been closed");
        }
        if(event.type == "open") {
            this.messages.push("/The socket connection has been established");
        }
    });
}

在上述方法中,我们订阅了在provider中创建的事件监听器。在这里我们需要检查发生了什么事件。如果是一条消息,需要检查是否存在发件人,然后将其添加到消息中。

你可能注意到了,一些消息是以斜线开始的。用来表示系统消息,稍后会将其加粗。

当客户端断开时,关闭事件将会发送到服务器,如果消息已经发送,它也会被发送到服务器。

在查看HTML之前,先添加一些CSS,使其看起来更像一个聊天应用。打开 src/style.css,输入以下内容:

/*src/styles.css*/
/* You can add global styles to this file, and also import other style files */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }

现在,需要处理下HTML了。打开 src/app/app.component.html 文件,并输入以下内容:

<!--src/app/app.component.html-->
<ul id="messages">
    <li *ngFor="let message of messages">
        <span [innerHTML]="isSystemMessage(message)"></span>
    </li>
</ul>
<form action="">
    <input [(ngModel)]="chatBox" [ngModelOptions]="{standalone: true}" autocomplete="off" />
    <button (click)="send()">Send</button>
</form>

这里我们只是简单的将消息数组遍历到屏幕上 。以斜线开头的消息将会被加粗。提交按钮绑定到了send方法中,当按下时,会提交输入框中的内容到Go应用。

结语

刚刚演示了如何使用 Go 和 Angular 2 创建一个 WebSocket 实时聊天应用。虽然没有在这个示例中存储聊天记录,但是这套逻辑可以应用于更复杂的项目,比如游戏,IOT,和其他很多场景。

关于原作者

Nic Raboy是现代网络和移动开发技术的倡导者。 他在Java,JavaScript,Golang以及各种框架(如Angular,NativeScript和Apache Cordova)方面拥有丰富的经验。 Nic写作的内容主要是他在使Web和移动开发更容易理解相关方面的经验。

在Go中使用 HTTP/2 Server Push

写在前面

本文来自Golang的官方博客,由 Jaana Burcu Dogan, Tom Bergan 发表于2017年3月24日。

近来看到此篇,觉得不错,非常适合用来学习Go中的 Server Push ,于是决定翻译一下,水平有限,如有不足不恰当的地方还请提出宝贵意见。

HTTP/2旨在解决HTTP/1.x的很多问题。现代网页通常包含很多资源:HTML,css,js,图片等等。在 HTTP/1.x 中,必须明确地请求这些资源。这是一个缓慢的过程。浏览器必须从获取HTML开始,然后在解析页面时获取更多资源。由于服务器必须等待浏览器发起请求,网络通常处于空闲,没有充分利用。

为了改善延迟,HTTP/2引入了 Server Push ,这允许服务器在明确的请求之前将资源推送到浏览器。服务器通常会知道一个页面所需要的额外的资源,并且可以在响应初始请求时开始推送这些资源。这就允许服务器充分利用空闲的网络来改善加载时间。

server push示意

在协议层,HTTP/2 Server Push 由 PUSH_PROMISE 帧发起。PUSH_PROMISE 表明了服务器向客户端推送资源的意图。一旦浏览器接收到PUSH_PROMISE,它就会知道服务器会推送资源。如果浏览器后来发现需要这个资源,它会等待推送完成,而不是发起一个新的请求。这减少了浏览器在网络上等待的时间。

net/http 包中的 Server Push

Go1.8 引入了 http.Server 对 push 响应的支持,如果运行的服务器是 HTTP/2 服务器,并且请求连接使用了 HTTP/2,则可以使用此功能。在任何HTTP处理程序中,可以通过检查 http.ResponseWriter 是否实现了 http.Pusher 接口来判断是否支持 Server Push。

例如,如果服务器知道一个页面中包含 app.js,处理程序可以初始化一个 http.Pusher。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  if push, ok := w.(http.Pusher); ok {
    //支持Push
    if err := pusher.Push("/app.js", nil); err != nil {
      log.Printf("Failed to push: %v", err)
    }
  }
  // ...
})

Push会为 app.js 创建一个 “合成请求”,将该请求合成到 PUSH_PROMISE 帧中,然后将请求转发到服务器的请求处理程序,请求处理程序会生成响应。Push的第二个参数指定了包含在 PUSH_PROMISE 中的附加header头,例如,如果对 app.js 的响应在 Accept-Encoding 上不同,则 PUSH_PROMISE 应包含 Accept-Encoding 的值。

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  if pusher, ok := w.(http.Pusher); ok {
    //支持Push
    options := &http.PushOptions{
      Header: http.Header{
        "Accept-Encoding": r.Header["Accept-Encoding"],
      },
    }
    if err := pusher.Pusher("/app.js", options); err != nil {
      log.Printf("Failed to push: %v", err)
    }
  }
  // ...
})

完整示例见:

$ go get golang.org/x/blog/content/h2push/server

启动服务,然后打开 https://localhost:8080,浏览器开发工具应该会显示服务器推送了 app.js 和 style.css 。

网络请求耗时

在响应前开始Push

在发送响应之前调用Push是个好主意,否则可能会意外产生重复的响应,例如,假设下边是HTML响应的一部分:

<html>
<head>
	<link rel="stylesheet" href="a.cs">...	

然后调用 Push(“a.css”, nil) ,浏览器可能会在接收到 PUSH_PROMISE 之前就开始解析这段 HTM L了,这种情况下,浏览器除了接收到 PUSH_PROMISE 之外,还会发起一个 a.css 的请求,那么服务器就会为 a.css 生成两个请求。而在写入响应之前调用PUSH则避免了这种可能性。

何时使用Server Push

应该考虑在任何网络连接空闲时使用 Server Push 。刚完成为 web app 发送 HTML ?不要浪费时间等待请求,开始推送客户端需要用到的资源,你有没有过将资源嵌入到 HTML 文件中以减少延迟?替换掉内联,尝试使用推送。重定向是另一个使用推送的好时机,因为客户端在这个过程中几乎把时间全浪费在请求的往返上。有很多情况适合使用 Push ,我们才刚刚开始。

如果没有提到以下几个注意事项,将是我们的失职。

​ 首先,只能推送当前服务器上的资源-这意味着无法推送托管在第三方服务器或CDN上的资源。

​ 第二,除非能确定客户端确实需要,否则不要推送,这样会浪费带宽。当浏览器已经缓存了某些资源时, 必须要避免推送。

​ 第三,天真的把所有资源推送到页面会使性能更糟糕。

以下链接可以作为补充阅读:

结尾

Go1.8 标准库为 HTTP/2 Server Push 提供了开箱即用的支持,为优化 Web 应用程序提供更多灵活性。

转到HTTP/2 Server Push演示页面查看实际效果。

一些资料

HTTP/2 简介

HTTP/2 Server Push 详解:

原文:

A Comprehensive Guide To HTTP/2 Server Push

译文:

HTTP/2 Server Push 详解(上)

HTTP/2 Server Push 详解(下)

MySQL实现按经纬度做距离排序

题图来自网络

工作中某些业务需要用到按距离排序返回结果,之前的方式是根据前端传过来来的经纬度,和指定范围的距离,算出一个坐标区间,再用这个区间的值去MySQL中查找,类似“where lat between (lat1, lat2) and lng between (lng1,lng2)”,查出数据后,再遍历数据计算每一条数据到这个经纬度的距离,然后根据得出的距离排序返回。低效,麻烦,不方便分页。

于是决定直接从MySQL中算出距离后返回,省事,方便,还可以直接分页了。

查资料后发现还挺简单的,下方的示例是从Google官方的文档中摘取出来。

创建如下数据表:

CREATE TABLE `markers` (
  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
  `name` VARCHAR( 60 ) NOT NULL ,
  `address` VARCHAR( 80 ) NOT NULL ,
  `lat` FLOAT( 10, 6 ) NOT NULL ,
  `lng` FLOAT( 10, 6 ) NOT NULL
) ENGINE = MYISAM ;

填充数据:

INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Frankie Johnnie & Luigo Too','939 W El Camino Real, Mountain View, CA','37.386339','-122.085823');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Amici\'s East Coast Pizzeria','790 Castro St, Mountain View, CA','37.38714','-122.083235');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Kapp\'s Pizza Bar & Grill','191 Castro St, Mountain View, CA','37.393885','-122.078916');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Round Table Pizza: Mountain View','570 N Shoreline Blvd, Mountain View, CA','37.402653','-122.079354');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Tony & Alba\'s Pizza & Pasta','619 Escuela Ave, Mountain View, CA','37.394011','-122.095528');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Oregano\'s Wood-Fired Pizza','4546 El Camino Real, Los Altos, CA','37.401724','-122.114646');

下面,开始从表中查询数据。

根据latitude,longitude值,基于Haversine公式从表中查询数据。

假设我们要查询latitude=37.38714,longitude=-122.083235,范围在25英里内的前20条数据,可以这样:

SELECT id, ( 3959 * acos( cos( radians('37.38714') ) * cos( radians( lat ) ) * cos( radians( lng ) - radians('-122.083235') ) + sin( radians('37.38714') ) * sin( radians( lat ) ) ) ) AS distance FROM markers HAVING distance < 25 ORDER BY distance LIMIT 0, 20;

如果想使用“公里”代替“英里”,将3959换成6371即可。

特别简单。

参考资料:

Creating a Store Locator with PHP, MySQL & Google Maps

Geo/Spatial Search with MySQL

Beego中使用过滤器

为了方便调试和排错,决定在现有的beego程序里加上requestID。

查了些资料发现写的并不是特别清楚和详细,在此总结一下,也算是加深下印象。

astaxie说可以用过滤器实现,就是在Beego运行时在特定的步骤前加入。而由于我的需求比较简单,就选在了BeforeRouter。

在main.go中:

import "github.com/astaxie/beego/context"
import "github.com/satori/go.uuid"

在main函数中加入:

var FilterRequestID = func(ctx *context.Context) {
	requestId := uuid.NewV4().String()
	ctx.Input.SetData("requestId", requestId)
}

beego.InsertFilter("/*", beego.BeforeRouter, FilterRequestID)

在需要使用的地方,如

// @router /requestid [get]
func (this *MyController) Requestid() {
	//读取requestId
	rid := this.Ctx.Input.GetData("requestId").(string)

	fmt.Println("requestId:",rid)
}

或者

func (m *MyController) Requestid() {
	rid := m.Ctx.Input.GetData("requestId").(string)

	fmt.Println("requestId:",rid)
}

其实很简单,但是文档和查到的资料中都没有明确的说需要引用 “github.com/astaxie/beego/context”,导致写的时候浪费了一些时间。

参考资料:

过滤器 beego log中增加request id的一种方式

Go语言写的一个短网址服务

题图来自http://www.dwtricks.com/

“缩址,又称短址、短网址、网址缩短、缩短网址、URL缩短等,指的是一种互联网上的技术与服务。此服务可以提供一个非常短小的URL以代替原来的可能较长的URL,将长的URL地址缩短。 用户访问缩短后的URL时,通常将会重定向到原来的URL。”

– Wikipedia

虽然短网址早已不再那么受广泛关注。但是不妨拿来练手。

根据公开可以搜索到的资料,短网址一般是将一个ID转换到一串字母,生成短的网址用于传播,实际访问会重定向到原网址。如上所述。

那么使用Go来写这个有什么优势呢,优势之一当然是,Go部署简单,只需要copy执行文件即可。执行速度也快,甚至连HTTP服务器都不需要。

下边就边写边说明。
package main

import (
	"fmt"
	"strings"
	"time"
	"net/http"
	"database/sql"

	"github.com/gin-gonic/gin"

	"github.com/garyburd/redigo/redis"
	_ "github.com/go-sql-driver/mysql"

	"github.com/speps/go-hashids"
)
定义hashid包需要的salt,即生成字符串的最短位数。
const (
	hdSalt        = "mysalt"
	hdMinLength   = 5
	defaultDomain = "http://localhost:8000/"
)
定义redis和MySQL的配置信息
var (
	RedisClient *redis.Pool
	RedisHost   = "127.0.0.1:6379"
	RedisDb     = 0
	RedisPwd    = ""

	db      *sql.DB
	DB_HOST = "tcp(127.0.0.1:3306)"
	DB_NAME = "short"
	DB_USER = "root"
	DB_PASS = ""
)
main函数,首先连接redis和MySQL。定义如下路由:
  • 访问首页
  • 访问hash
  • 访问短网址信息页
  • 生成短网址接口

熟悉的朋友应该都知道,访问短网址服务的首页一般会跳转到一个固定的网址,比如渣浪微博会跳转到微博首页,Twitter则是给出“Twitter uses the t.co domain as part of a service to protect users from harmful activity”的提示。这里我们也让它跳转到一个指定的网页。

最后,以8080端口运行,实际线上会使用80端口,可以自行修改。

func main() {
	initRedis()
	initMysql()

	gin.SetMode(gin.DebugMode)
	r := gin.Default()

	r.GET("/", func(c *gin.Context) {
		//http code can be StatusFound or StatusMovedPermanently 
		c.Redirect(http.StatusFound, defaultDomain)
	})
	r.GET("/:hash", expandUrl)
	r.GET("/:hash/info", expandUrlApi)
	r.POST("/short", shortUrl)

	r.Run(":8000")
}
连接redis和MySQL
func initRedis() {
	// 建立连接池
	RedisClient = &redis.Pool{
		MaxIdle:     1,
		MaxActive:   10,
		IdleTimeout: 180 * time.Second,
		Dial: func() (redis.Conn, error) {
			c, err := redis.Dial("tcp", RedisHost)
			if err != nil {
				return nil, err
			}
			if _, err := c.Do("AUTH", RedisPwd); err != nil {
				c.Close()
				return nil, err
			}
			c.Do("SELECT", RedisDb)
			return c, nil
		},
	}
}

func initMysql() {
	dsn := DB_USER + ":" + DB_PASS + "@" + DB_HOST + "/" + DB_NAME + "?charset=utf8"
	db, _ = sql.Open("mysql", dsn)
	db.SetMaxOpenConns(5)
	db.SetMaxIdleConns(20)
	db.Ping()
}
生成短网址的接口函数。

根据传入的URL参数,进行简单的验证后,写入数据库。根据写入后生成的ID,再生成一个字符串,然后返回给调用方。

func shortUrl(c *gin.Context) {
	longUrl := c.PostForm("url")

	if longUrl == "" {
		c.JSON(200, gin.H{
			"status":  500,
			"message": "请传入网址",
		})
		return
	}

	if !strings.HasPrefix(longUrl, "http") {
		longUrl = "http://" + longUrl
	}

	if hash, ok := insert(longUrl); ok {
		c.JSON(200, gin.H{
			"status":  200,
			"message": "ok",
			"short":   defaultDomain + hash,
		})
	}
}
根据HASH解析并跳转到对应的长URL,不存在则跳转到默认地址
func expandUrl(c *gin.Context) {
	hash := c.Param("hash")

	if url, ok := findByHash(hash); ok {
		c.Redirect(http.StatusMovedPermanently, url)
	}
	// 注意:
	// 	实际中,此应用的运行域名可能与默认域名不同,如a.com运行此程序,默认域名为b.com
	// 	当访问一个不存在的HASH或a.com时,可以跳转到任意域名,即defaultDomain
	c.Redirect(http.StatusMovedPermanently, defaultDomain)
}
根据HASH在redis中查找并返回结果,不存在则返回404状态
func expandUrlApi(c *gin.Context) {
	hash := c.Param("hash")

	if url, ok := findByHash(hash); ok {
		c.JSON(200, gin.H{
			"status":  200,
			"message": "ok",
			"data":    url,
		})
		return
	}

	// 此处可以尝试在MySQL中再次查询
	c.JSON(200, gin.H{
		"status":  404,
		"message": "url of hash is not exist",
	})
}
将ID转换成对应的HASH值,hdSalt与hdMinLength 会影响生成结果,确定后不要改动
func shortenURL(id int) string {
	hd := hashids.NewData()
	hd.Salt = hdSalt
	hd.MinLength = hdMinLength

	h := hashids.NewWithData(hd)
	e, _ := h.Encode([]int{id})

	return e
}
根据HASH解析出对应的ID值, hdSalt与hdMinLength 会影响生成结果,确定后不要改动
func expand(hash string) int {
	hd := hashids.NewData()
	hd.Salt = hdSalt
	hd.MinLength = hdMinLength

	h := hashids.NewWithData(hd)
	d, _ := h.DecodeWithError(hash)

	return d[0]
}
数据库中根据ID查找
func find(id int) (string, bool) {
	var url string
	err := db.QueryRow("SELECT url FROM url WHERE id = ?", id).Scan(&url)
	if err == nil {
		return url, true
	} else {
		return "", false
	}
}
在redis中根据HASH查找
func findByHash(h string) (string, bool) {
	rc := RedisClient.Get()

	defer rc.Close()
	url, _ := redis.String(rc.Do("GET", "URL:"+h))

	if url != "" {
		return url, true
	}

	id := expand(h)
	if urldb, ok := find(id); ok {
		return urldb, true
	}

	return "", false
}
将长网址插入到数据库中,并把返回的ID生成HASH和长网址存入redis
func insert(url string) (string, bool) {
	stmt, _ := db.Prepare(`INSERT INTO url (url) values (?)`)
	res, err := stmt.Exec(url)
	checkErr(err)

	id, _ := res.LastInsertId()

	rc := RedisClient.Get()
	defer rc.Close()

	hash := shortenURL(int(id))
	rc.Do("SET", "URL:"+hash, url)

	return hash, true
}

打印方法,和检查错误的方法

func Log(v ...interface{}) {
	fmt.Println(v...)
}

func checkErr(err error) {
	if err != nil {
		panic(err)
	}
}

有些地方还需修改,就算是抛砖引玉吧。

感谢hashids

Github地址 : shortme

相关资料:

URL Toolbox: 90+ URL Shortening Services TinyURL

Go 福利小爬虫 爬取今日头条美女图

写完爬取糗百热门后没几天,又开始写了爬取今日头条图片的工具

灵感来源于Python 福利小爬虫,爬取今日头条街拍美女图,作者很详细的分析了今日头条一个搜索接口,并列出了步骤。

而我用Go写的,稍稍做了改动,加入了可以自定义爬取标签的功能,并在写本文前完成了以 “标签/文章名/图片名” 结构存储图片的功能。

分析网页依然使用goquery

分析接口返回结构

{
	"count": 30,
	"action_label": "click_search",
	"return_count": 0,
	"has_more": 0,
	"page_id": "/search/",
	"cur_tab": 1,
	"offset": 150,
	"action_label_web": "click_search",
	"show_tabs": 1,
	"data": [
		{
			"play_effective_count": "6412",
			"media_name": "开物志",
			"repin_count": 49,
			"ban_comment": 0,
			"show_play_effective_count": 1,
			"abstract": "",
			"display_title": "",
			"datetime": "2016-12-13 21:35",
			"article_type": 0,
			"more_mode": false,
			"create_time": 1481636117,
			"has_m3u8_video": 0,
			"keywords": "",
			"video_duration": 161,
			"has_mp4_video": 0,
			"favorite_count": 49,
			"aggr_type": 0,
			"article_sub_type": 0,
			"bury_count": 2,
			"title": "沃尔沃Tier 4 Final大型引擎的工作原理揭秘",
			"has_video": true,
			"share_url": "http://toutiao.com/group/6363577276176531969/?iid=0&app=news_article",
			"id": 6363577276176532000,
			"source": "开物志",
			"comment_count": 4,
			"article_url": "http://toutiao.com/group/6363577276176531969/",
			"image_url": "http://p3.pstatp.com/list/12f0000909de79ceeabc",
			"middle_mode": true,
			"large_mode": false,
			"item_source_url": "/group/6363577276176531969/",
			"media_url": "http://toutiao.com/m6643043415/",
			"display_time": 1481635793,
			"publish_time": 1481635793,
			"go_detail_count": 2290,
			"image_list": [],
			"item_seo_url": "/group/6363577276176531969/",
			"video_duration_str": "02:41",
			"source_url": "/group/6363577276176531969/",
			"tag_id": 6363577276176532000,
			"natant_level": 0,
			"seo_url": "/group/6363577276176531969/",
			"display_url": "http://toutiao.com/group/6363577276176531969/",
			"url": "http://toutiao.com/group/6363577276176531969/",
			"level": 0,
			"digg_count": 4,
			"behot_time": 1481635793,
			"tag": "news_car",
			"has_gallery": false,
			"has_image": false,
			"highlight": {
			"source": [],
			"abstract": [],
			"title": []
			},
			"group_id": 6363577276176532000,
			"middle_image": "http://p3.pstatp.com/list/12f0000909de79ceeabc"
		},
	],
	"message": "success",
	"action_label_pgc": "click_search"
}

嗯,特别多,其实只需要 data 里的内容就可以了。

所以

构造一个请求结果的struct。

type ApiData struct {
	Has_more int    `json:"has_more"`
	Data     []Data `json:"data"`
}

再看下data里,嗯,没用的又一大堆。

只需要文章链接就够了。

type Data struct {
	Article_url string `json:"article_url"`
}

有了文章链接,那就好说了,啥都好商量。

分析文章结构

id=“J_content” 下是文章的主要内容,class=“article-title”是文章标题,class=“article-content”里是文章内容,只需要article-content里所有img元素就可以了。

type Img struct {
	Src string `json:"src"`
}

由于需要一直更改查询接口的offset参数,所以直接把接口地址拿到外边做了全局变量。并且默认存在下一页。tag用来表示当前爬取的标签的名称。

var (
	host    string = "http://www.toutiao.com/search_content/?format=json&keyword=%s&count=30&offset=%d"
	hasmore bool   = true
	tag     string
)

正菜

0. 接收参数

首先,接收并遍历命令行中传入的标签。

func main() {
	for _, tag = range os.Args[1:] {
		hasmore = true
		getByTag()
	}
	log.Println("全部抓取完毕")
}

每个循环开始时重置 hasmore 。

1. 循环请求接口

func getByTag() {
	i, offset := 1, 0
	for {
		if hasmore {
			log.Printf("标签: '%s',第 '%d' 页, OFFSET: '%d' \n", tag, i, offset)
			tmpUrl := fmt.Sprintf(host, tag, offset)
			getResFromApi(tmpUrl)
			offset += 30
			i++

			time.Sleep(500 * time.Millisecond)
		} else {
			break
		}
	}
	log.Printf("标签: '%s', 共 %v 页,爬取完毕\n", tag, i-1)
}

重置当前页,和当前offset。页数从第一页开始,主要是显示进度看起来更人性化一些。但是程序员的世界是从0开始。。。想改成0就改成0吧。

hasmore = true 表示存在下一页,使用fmt包的Sprintf方法格式化请求链接。然后对offset+30,对当前页i+1。再之后停顿了500毫秒。

这里其实有个问题,如果实际内容以每页30请求,可能恰好有150条,即每页数量的整数倍,但是这个时候接口返回的has_more依然等于1,即服务端认为还有下一页。。。但是其实没有了,所以会有一次空循环。

2. 处理请求结果

func getResFromApi(url string) {
	resp, err := http.Get(url)
	if err != nil {
		log.Fatal(err)
	}

	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		log.Fatal(err)
	}

	var res ApiData
	json.Unmarshal([]byte(string(body)), &res)

	for _, item := range res.Data {
		getImgByPage(item.Article_url)
	}

	if res.Has_more == 0 {
		hasmore = false
	}
}

没啥说的,拿到每一个请求接口的链接后打开,把结果数组中的data解析到ApiData中,于是就拿到了文章链接,然后遍历处理。

遍历完后要看下has_more的值,如果为0表示没有下一页了,修改全局变量hasmore的值,结束最外层的循环。

3. 处理文章

func getImgByPage(url string) {
	//部分请求结果中包含其他网站的链接,会导致下面的query出现问题
	if strings.Contains(url, "toutiao.com") {
		doc, err := goquery.NewDocument(url)
		if err != nil {
			log.Fatal(err)
		}

		title := doc.Find("#article-main .article-title").Text()
		title = strings.Replace(title, "/", "", -1)
		os.MkdirAll(tag+"/"+title, 0777)

		doc.Find("#J_content .article-content img").Each(func(i int, s *goquery.Selection) {
			src, _ := s.Attr("src")
			log.Println(title, src)
			getImgAndSave(src, title)
		})
	}
}

最外层加了判断,是因为有一部分结果的链接是其他网站的。。。。

虽然这个判断很low,但是也够用了。

然后终于该用上goquery了,拿到标题,然后遍历文章内容中的img标签,就拿到了每一篇文章的每一张图片。

4. 保存图片

在上一步把图片地址和文章名称传递给了getImgAndSave。

func getImgAndSave(url string, dirname string) {
	path := strings.Split(url, "/")
	var name string
	if len(path) > 1 {
		name = path[len(path)-1]
	}

	resp, err := http.Get(url)
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Fatal("请求失败", err)
		return
	}

	contents, err := ioutil.ReadAll(resp.Body)
	defer func() {
		if x := recover(); x != nil {
			return
		}
	}()
	err = ioutil.WriteFile("./"+tag+"/"+dirname+"/"+name+".jpg", contents, 0644)
	if err != nil {
		log.Fatal("写入文件失败", err)
	}
}

先分割图片链接,把最后一个”/“后的内容当成文件名。

后边get图片内容,但是有时候会出现对方服务器出错的情况,http状态码为500,所以加了判断请求是否成功的判断。

然后就是读取内容,保存到文件中了。

这里使用了WriteFile方式,查资料的时候还看到有闲Create文件,然后io.Copy写入的。

到这里就结束了。

RUN

go run main.go 美女 模特

等着看图吧。

github地址:toutiaoSpider,欢迎star。

Go语言写爬取糗百热门帖子

闲来无事,想着也用Go来写个爬虫之类的东西,我并不知道这算不算严格意义上的爬虫。

思前想后,觉得写个爬糗百热门的脚本吧,一来足够简单,二来大概熟悉下流程。

首先,选了goquery这个包来解析HTML,声称与jquery相似的用法,事实上也确实是这样,非常方便。

定个目标,只爬取列表页的帖子内容,作者和回帖都不管。

package main

import (
	"github.com/PuerkitoBio/goquery"
	"log"
)

//定义结构体
type Qb struct {
	Id int `json:"id"`
	Content string `json:"content"`
}

func main() {
	var url = "http://www.qiushibaike.com/hot"

	doc, err := goquery.NewDocument(url)
	if err != nil {
		log.Fatal(err)
	}

	var qb []Qb
	doc.Find("#content-left .article").Each(func(i int, s *goquery.Selection) {
		//s即为当前的 .article 元素,查找下级中的span元素的内容。
		content := s.Find(".content span").Text()
		qb = append(qb, Qb{Id: i, Content: content})
	})

	log.Println(qb)
}

”#content-left .article” 即每一条帖子作为元素的class。

将会输出:

[
	{0 结婚十三周年那天,老婆望着一大桌子菜不禁泪流满面。我帮她拭去泪水:瞧你,都激动的哭了!老婆却说:我激动个屁!想想这十三年跟着你受的罪,我实在忍不住啊!} 
	{1 前几天天冷,就给妹妹买了条围巾,然后她说谢谢哥,本人本着组织精神说你应该谢谢你嫂子,她惊讶的对我说:哥,你谈女朋友了。我说:没有,你应该感谢她一直到现在都没出现,哥才有钱给你买东西} 
	{2 跟哥们去理发,剪头的是个妹纸。。妹纸:“你有女朋友么?”哥们一听,突然兴奋的说:“没有!”妹纸:“我是个实习生,本来想给你换大工的,看你没有女朋友,我就随意剪了!”哥们你别看我,我就是一口水没忍住,喷你脸上了而已!} 
	{3 老妈比较胖,小时候每次打我我都是撒腿就跑,老妈没一次抓到我的。直到老妈学会骑自行车以后,那鞭子挥得………真像套马杆的汉子,威武雄壮……}
]

那么如何展示到页面中呢。

我选择了 gin 框架。

修改一下代码。

func main() {
	r := gin.Default()
	r.LoadHTMLGlob("public/*")
	r.GET("/", Index)
	r.Run()
}

func Index(c *gin.Context) {
	var url = "http://www.qiushibaike.com/hot"

	doc, err := goquery.NewDocument(url)
	if err != nil {
		log.Fatal(err)
	}

	var result []Qb
	doc.Find("#content-left .article").Each(func(i int, s *goquery.Selection) {
		content := s.Find(".content span").Text()
		result = append(result, Qb{Id: i, Content: content})
	})

	c.HTML(http.StatusOK, "index.html", gin.H{
		"items": result,
		"title": "糗百热门"
	})
}

可以看到,

r := gin.Default()
r.LoadHTMLGlob("public/*")
r.GET("/", Index)

这里加载了public目录中的模板,然后下一行,表示,接收到 “/” 的请求时,调用Index方法去处理。

到这里,文档的抓取,解析,构造数据就已经完成,下一步,看一下怎么显示到页面中。

{% raw %}
<div class="col-md-12">
    <h2>{{ .title }}</h2>
    <table class="table table-striped table-bordered table-hover">
        {{ range $item := .items }}
        <tr>
            <td>{{ $item.Content }}</td>
        </tr>
        {{ end }}
    </table>
</div>
{% endraw %}

使用 “{% raw %}{{ }}{% endraw %}” 输出后端发送过来的数据。使用 range 迭代数据。与

for pos, char := range str {
...
}

一样。

完整的模板代码:

{% raw %}
<!-- public/index.html -->

<html>
    <head>
        <meta http-equiv="content-type" content="text/html; charset=utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
        <title>糗百</title>

        <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.6/css/bootstrap.min.css">
        <link rel="stylesheet"  href="https://cdn.bootcss.com/font-awesome/4.6.3/css/font-awesome.min.css">

    </head>
    <body>
        <div class="container">
            <div class="row">
                <div class="col-md-12">
                    <h2>{{ .title }}</h2>
                    <table class="table table-striped table-bordered table-hover">
                        {{ range $item := .items }}
                        <tr>
                            <td>{{ $item.Content }}</td>
                        </tr>
                        {{ end }}
                    </table>
                </div>
            </div>
        </div>
    </body>
</html>
{% endraw %}

这样,运行一下,就可以了。

gin框架默认使用8080端口,打开 http://localhost:8080 就可以看到一个极简版的糗百热门了。

问题来了,怎么增加一个分页呢?

完整代码见:

Github地址

后记

其实早就写完了这篇,但是hexo生成的时候由于 ”{% raw %}{{{% endraw %}“的问题,生成一直失败,一直拖到现在。

实际代码中需要去掉 “{ % raw % }” 相关部分。