实践是最好的学习方式

零基础通过开发Web服务学习Go语言

本文适合有一定编程基础,但是没有Go语言基础的同学。

也就是俗称的“骗你”学Go语言系列。

这是一个适合阅读的系列,我希望您能够在车上、厕所、餐厅都阅读它,涉及代码的部分也是精简而实用的。

学习需要动机

Go语言能干什么?为什么要学习Go语言?

本系列文章,将会以编程开发中需求最大、应用最广的Web开发为例,一步一步的学习Go语言。当看完本系列,您能够清晰的了解Go语言Web开发的基本原理,您会惊叹于Go语言的简洁、高效和新鲜。

结果反馈才能让你记住

《刻意练习》一书中说,学习需要及时反馈结果,才能提高学习体验。

本系列文章的每一节,都会包含一段可运行的有效代码,跟着内容一步一步操作,你可以在你自己的计算机上体验每一句代码的作用。

不要学习不需要的东西

文章围绕范例为核心,介绍知识点。文中不罗列语法和关键字,当您还不知道它们用来干什么时,反而会干扰您的注意力。

希望您在阅读本系列文章后,对Go语言产生更多的学习欲望,成为一名合格的Gopher

Gopher:原译是囊地鼠,也就是Go语言Logo的那个小可爱;这里特指Go程序员给自己的昵称。

如何10分钟搭建Go开发环境

1.下载Go语言安装文件

访问Go语言官方网站下载页面:

可以看到官网提供了Microsoft Windows、Apple MacOS、Linux和Source下载。

直接下载对应操作系统的安装包。

2.和其他软件一样,根据提示安装

3.配置环境变量

在正式使用Go编写代码之前,还有一个重要的“环境变量”需要配置:“$GOPATH”

GOPATH环境变量指定工作区的位置。如果没有设置GOPATH,则假定在Unix系统上为$HOME/go,在Windows上为 %USERPROFILE%\go。如果要将自定义位置用作工作空间,可以设置GOPATH环境变量。

GOPATH环境变量是用于设置Go编译可以执行文件、包源码以及依赖包所必要的工作目录路径,Go1.11后,新的木块管理虽然可以不再依赖 $GOPATH/src,但是依然需要使用 $GOPATH/pkg 路径来保存依赖包。

首先,创建好一个目录用作GOPATH目录

然后设置环境变量 GOPATH:

Linux & MacOS:

导入环境变量

$ export GOPATH=$YOUR_PATH/go

保存环境变量

$ source ~/.bash_profile

Windows:

控制面板->系统->高级系统设置->高级->环境变量设置

$GOPATH设置好后,它是一个空目录,当在开发工作中执行go get、go install命令后, $GOPATH所指定的目录会生成3个子目录:

bin:存放 go install 编译的可执行二进制文件

pkg:存放 go install 编译后的包文件,就会存放在这里

src:存放 go get 命令下载的源码包文件

4.检查环境

打开命令行工具,运行

$ go env

如果你看到类似这样的结果,说明Go语言环境安装完成.

GOARCH="amd64"

GOBIN=""

GOCACHE="/Users/zeta/Library/Caches/go-build"

GOEXE=""

GOFLAGS=""

GOHOSTARCH="amd64"

GOHOSTOS="darwin"

GOOS="darwin"

GOPATH="/Users/zeta/workspace/go"

GOPROXY="https://goproxy.io"

GORACE=""

GOROOT="/usr/local/go"

GOTMPDIR=""

GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"

GCCGO="gccgo"

CC="clang"

CXX="clang++"

CGO_ENABLED="1"

GOMOD=""

CGO_CFLAGS="-g -O2"

CGO_CPPFLAGS=""

CGO_CXXFLAGS="-g -O2"

CGO_FFLAGS="-g -O2"

CGO_LDFLAGS="-g -O2"

PKG_CONFIG="pkg-config"

GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/7v/omg2000000000000019/T/go-build760324613=/tmp/go-build -gno-record-gcc-switches -fno-common"

5.选择一款趁手的编辑器或IDE

现在很多通用的编辑器或IDE都支持Go语言比如

Go语言专用的IDE有

专用的IDE无论是配置和使用都比通用编辑器/IDE的简单许多,但是我还是推荐大家使用通用编辑器/IDE,因为在开发过程中肯定会需要编写一些其他语言的程序或脚本,专用IDE在其他语言编写方面较弱,来回切换不同的编辑器/IDE窗口会很低效。

另外,专用IDE提供很多高效的工具,在编译、调试方面都很方便,但是学习阶段,建议大家手动执行命令编译、调试,有利于掌握Go语言。

四行代码的Hello World!所能表达出来的核心

命令行代码仅适用于Linux和MacOS系统,Windows根据说明在视窗下操作即可。

1.创建项目

创建一个文件夹,进入该文件夹

$ mkdir gowebserver && cd gowebserver

新建一个文件 main.go

$ touch main.go

2. 用编辑器打开文件,并输入以下代码:

package main

import "fmt"

func main() {

fmt.Println("Hello, 世界")

}

3.打开命令行终端,输入以下命令

$ go run main.go

看到终端会输出:

Hello, 世界

第一个Go代码就完成了

这是一个很简单的Hello World,但是包含了Go语言编程的许多核心元素,接下来就详细讲解。

解读知识点: 包 与 函数

package申明包 & import导入包

Go程序是由包构成的。

代码的第一行, 申明程序自己的包,用 package 关键字。package关键字必须是第一行出现的代码。

范例代码中,申明的本包名 main

在代码中第二行, 导入“fmt”包, 使用 import 关键字。默认情况下,导入包的包名与导入路径的最后一个元素一致,例如 import "math/rand",在代码中使用这个包时,直接使用rand,例如 rand.New()

导入包的写法可以多行,也可以“分组”, 例如:

import "fmt"

import "math/rand"

或者 分组

import (

"fmt"

"math/rand"

)

fmt包是Go语言内建的包,作用是输出打印。

func关键字:定义函数

func是function的缩写, 在Go语言中是定义函数的关键字。

func定义函数的格式为:

func 函数名(参数1 类型,参数2 类型){

函数体

}

本例中定义了一个main函数。main函数没有参数。

然后在main函数体里调用fmt包的Println函数,在控制台输出字符串 “Hello, 世界”

所有Go语言的程序的入口都是main包下的main函数 main.main(),所以每一个可执行的Go程序都应该有一个main包和一个main函数。

我们已经介绍了九牛一毛中的一毛,接下来正式通过搭建一个简单的Web服务学习Go语言

0依赖,创建一个Web服务

先从代码开始

打开之前创建好的main.go文件,修改代码如下:

package main

import (

"fmt"

"net/http"

)

func myWeb(w http.ResponseWriter, r *http.Request) {

fmt.Fprintf(w, "这是一个开始")

}

func main() {

http.HandleFunc("/", myWeb)

fmt.Println("服务器即将开启,访问地址 http://localhost:8080")

err := http.ListenAndServe(":8080", nil)

if err != nil {

fmt.Println("服务器开启错误: ", err)

}

}

保存文件,然后在命令行工具下输入命令,运行程序

$ go run main.go

这时候,你会看到用 fmt.Println打印出来的提示,在浏览器中访问 http://localhost:8080你将访问到一个页面,显示 "这是一个开始"

解读

我们从程序运行的顺序去了解它的工作流程

首先,定义package main,然后导入包。

这里,导入了一个新的包 net/http,这个包是官方的,实现http客户端和服务端的各种功能。Go语言开发Web服务的所有功能就是基于这个包(其他第三方的Go语言Web框架也都基于这个包,没有例外)

先看main函数里发生了什么

第一句,匹配路由和处理函数

http.HandleFunc("/", myWeb)

调用http包的HandleFunc方法,匹配一个路由到一个处理函数myWeb。

这句代码的意思是,当通过访问地址 http://localhost/ 时,就等同于调用了 myWeb 函数。

第二句,用fmt在控制台打印一句话,纯属提示。

第三句,开启服务并且监听端口

err := http.ListenAndServe(":8080", nil)

在这句,调用了http包中的ListenAndServe函数,该函数有两个参数,第一个是指定监听的端口号,第二个是指定处理请求的handler,通常这个参数填nil,表示使用默认的ServeMux作为handler。

什么是nil?

nil就是其他语言里的null。

什么是handler?什么是ServeMux?

ServeMux就是一个HTTP请求多路由复用器。它将每个传入请求的URL与已注册模式的列表进行匹配,并调用与URL最匹配的模式的处理程序。

很熟悉吧?还记得前面的http.HandleFunc吗?他就是给http包中默认的ServeMux(DefaultServeMux)添加URL与处理函数匹配。

通常都是使用http包中的默认ServeMux,所以在http.ListenAndServe函数的第二个参数提供nil就可以了

ListenAndServe函数会一直监听,除非强制退出或者出现错误。

如果这句开启监听出现错误,函数会退出监听并会返回一个error类型的对象,因此用err变量接收返回对象。紧接着,判断err是否为空,打印出错误内容,程序结束。

这里有两个Go语言知识点

1.定义变量

Go语言是静态语言,需要定义变量,定义变量用关键字var

var str string = "my string"

//^ ^ ^

//关键字 变量名 类型

Go还提了一种简单的变量定义方式:=,自动根据赋值的对象定义变量类型,用起来很像脚本语言:

str := "my string"

2.错误处理

if err != nil{

//处理....

}

在Go语言中,这是很常见的错误处理操作,另一种panic异常,官方建议不要使用或尽量少用,暂不做介绍,先从err开始。

Go语言中规定,如果函数可能出现错误,应该返回一个error对象,这个对象至少包含一个Error()方法错误信息。

因此,在Go中,是看不到try/catch语句的,函数使用error传递错误,用if语句判断错误对象并且处理错误。

3. if 语句

与大多数语言使用方式一样,唯一的区别是,表达式不需要()包起来。

另外,Go语言中的if可以嵌入一个表达式,用;号隔开,例如范例中的代码可以改为:

if err := http.ListenAndServe(":8080", nil); err != nil {

fmt.Println("服务器开启错误: ", err)

}

err这个变量的生命周期只在if块中有效。

请求处理 myWeb函数

在main函数中,用http.HandleFunc将 myWeb与路由/匹配在一起。

HandleFunc函数定义了两个参数w,r,参数类型分别是http.ResponseWriter和*http.Request,w是响应留写入器,r是请求对象的指针。

响应流写入器 w: 用来写入http响应数据

请求对象 * r: 包含了http请求所有信息,注意,这里使用了指针,在定义参数时用*标记类型,说明这个参数需要的是这个类型的对象的指针。

当有请求路径/,请求对象和响应流写入器被传递给myWeb函数,并由myWeb函数负责处理这次请求。

Go语言中红的指针: 在Go语言中 除了map、slice、chan 其他函数传参都是值传递,所以,如果需要达到引用传递的效果,通过传递对象的指针实现。在Go语言中,取对象的指针用&,取值用*,例如:

mystring := "hi"

//取指针

mypointer := &mystring

//取值

mystring2 := *mypointer

fmt.Println(mystring,mypointer,mystring2)

把这些代码放在main函数里,$ go run main.go运行看看

myWeb函数体

fmt.Fprintf(w, "这是一个开始")

再一次遇到老熟人fmt,这次使用他的Fprintf函数将字符串“这是一个开始”,写入到w响应流写入器对象。w响应流写入器里写入的内容最后会被Response输出到用户浏览器的页面上。

总结一下,从编码到运行,你和它都干了些什么:

定义一个函数myWeb,接收参数 响应流写入器和请求对象两个参数

在main函数中,在默认的ServeMux中将路由/与myWeb绑定

运行默认的ServeMux监听本地8080端口

访问本地8080端口 / 路由

http将请求对象和响应写入器都传递给myWeb处理

myWeb向响应流中写入一句话,结束这次请求。

虽然代码很少很少,但是这就是一个最基本的Go语言Web服务程序了。

Web互动第一步,Go http 获得请求参数

还是先从代码开始

打开main.go文件,修改myWeb函数,如下:

func myWeb(w http.ResponseWriter, r *http.Request) {

r.ParseForm() //它还将请求主体解析为表单,获得POST Form表单数据,必须先调用这个函数

for k, v := range r.URL.Query() {

fmt.Println("key:", k, ", value:", v[0])

}

for k, v := range r.PostForm {

fmt.Println("key:", k, ", value:", v[0])

}

fmt.Fprintln(w, "这是一个开始")

}

运行程序

$ go run main.go

然后用任何工具(推荐Postman)提交一个POST请求,并且带上URL参数,或者在命令行中用cURL提交

curl --request POST \

--url 'http://localhost:8080/?name=zeta' \

--header 'cache-control: no-cache' \

--header 'content-type: application/x-www-form-urlencoded' \

--data description=hello

页面和终端命令行工具会答应出以下内容:

key: name , value: zeta

key: description , value: hello

解读

http请求的所有内容,都保存在http.Request对象中,也就是myWeb获得的参数 r 。

首先,调用r.ParseForm(),作用是填充数据到 r.Form 和 r.PostForm

接下来,分别循环获取遍历打印出 r.URL.Query() 函数返回的值 和 r.PostForm 值里的每一个参数。

r.URL.Query() 和 r.PostForm 分别是URL参数对象和表单参数对象

,它们都是键值对值,键的类型是字符串string,值的类型是string数组。

在http协议中,无论URL和表单,相同名称的参数会组成数组。

循环遍历:for...range

Go语言的循环只有for关键字,以下是Go中4种for循环

//无限循环,阻塞线程,用不停息,慎用!

for{

}

//条件循环,如果a

for a < b{

}

//表达式循环,设i为0,i小于10时循环,每轮循环后i增加1

for i:=0; i<10; i++{

}

//for...range 遍历objs,objs必须是map、slice、chan类型

for k, v := range objs{

}

前3种,循环你可以看作条件循环的变体(无限循环就是无条件的循环)。

本例种用到的是 for...range 循环,遍历可遍历对象,并且每轮循环都会将键和值分别赋值给变量 k 和 v

我们页面还是只是输出一句“这是一个开始”。我们需要一个可以见人的页面,这样可以不行

你也许也想到了,是不是可以在输出时,硬编码HTML字符串?当然可以,但是Go http包提供了更好的方式,HTML模版。

接下来,我们就用HTML模版做一个真正的页面出来

动态响应数据给访客,Go http HTML模版+数据绑定

读取HTML模版文件,用数据替换掉对应的标签,生成完整的HTML字符串,响应给浏览器,这是所有Web开发框架的常规操作。Go也是这么干的。

Go html包提供了这样的功能:

"html/template"

从代码开始

main函数不变,增加导入html/template包,然后修改myWeb函数,如下:

import (

"fmt"

"net/http"

"text/template" //导入模版包

)

func myWeb(w http.ResponseWriter, r *http.Request) {

t := template.New("index")

t.Parse("

Hi,{{.name}},{{.someStr}}

")

data := map[string]string{

"name": "zeta",

"someStr": "这是一个开始",

}

t.Execute(w, data)

// fmt.Fprintln(w, "这是一个开始")

}

在命令行中运行 $ go run main.go ,访问 http://localhost:8080

看,

Hi,{{.name}},{{.someStr}}

中的{{.name}}和{{.someStr}} 被替换成了 zeta和这是一个开始。并且,不再使用fmt.Fprintln函数输出数据到Response了

但是...这还是在代码里硬编码HTML字符串啊...

别着急,template包可以解析文件,继续修改代码:

根目录下创建一个子目录存放模版文件 templates, 然后进入目录创建一个文件 index.html,并写入一些HTML代码 (我不是个好前端)

Hello {{.name}}
{{.someStr}}

修改myWeb函数

func myWeb(w http.ResponseWriter, r *http.Request) {

//t := template.New("index")

//t.Parse("

Hi,{{.name}},{{.someStr}}

")

//将上两句注释掉,用下面一句

t, _ := template.ParseFiles("./templates/index.html")

data := map[string]string{

"name": "zeta",

"someStr": "这是一个开始",

}

t.Execute(w, data)

// fmt.Fprintln(w, "这是一个开始")

}

在运行一下看看,页面按照HTML文件的内容输出了,并且{{.name}}和{{.someStr}}也替换了,对吧?

解读

可以看到,template包的核心功能就是将HTML字符串解析暂存起来,然后调用Execute的时候,用数据替换掉HTML字符串中的{{}}里面的内容

在第一个方式中 t:=template.New("index") 初始化一个template对象变量,然后用调用t.Parse函数解析字符串模版。

然后,创建一个map对象,渲染的时候会用到。

最后,调用t.Execute函数,不仅用数据渲染模版,还替代了fmt.Fprintln函数的工作,将输出到Response数据流写入器中。

第二个方式中,直接调用 template包的ParseFiles函数,直接解析相对路径下的index.html文件并创建对象变量。

知识点

本节出现了两个新东西 map类型 和 赋值给“_”

map类型

map类型: 字典类型(键值对),之前的获取请求参数章节中出现的 url/values类型其实就是从map类型中扩展出来的

map的初始化可以使用make:

var data = make(map[string]string)

data = map[string]string{}

make是内置函数,只能用来初始化 map、slice 和 chan,并且make函数和另一个内置函数new不同点在于,它返回的并不是指针,而只是一个类型。

map赋值于其他语言的字典对象相同,取值有两种方式,请看下面的代码:

data["name"]="zeta" //赋值

name := data["name"] //方式1.普通取值

name,ok := data["name"] //方式2.如果不存在name键,ok为false

代码中的变量ok,可以用来判断这一项是否设置过,取值时如果项不存在,是不会异常的,取出来的值为该类型的零值,比如 int类型的值,不存在的项就为0;string类型的值不存在就为空字符串,所以通过值是否为0值是不能判断该项是否设置过的。

ok,会获得true 或者 false,判断该项是否设置过,true为存在,false为不存在于map中。

Go中的map还有几个特点需要了解:

map的项的顺序是不固定的,每次遍历排列的顺序都是不同的,所以不能用顺序判断内容

map可以用for...range 遍历

map在函数参数中是引用传递(Go语言中,只有map、slice、chan是引用传递,其他都是值传递)

赋值给 “_”

Go有一个特点,变量定义后如果没使用,会报错,无法编译。一般情况下没什么问题,但是极少情况下,我们调用函数,但是并不需要使用返回值,但是不使用,又无法编译,怎么办?

"_" 就是用来解决这个问题的,_用来丢弃函数的返回值。比如本例中,template.ParseFiles("./templates/index.html") 除了返回模版对象外,还会返回一个error对象,但是这样简单的例子,出错的可能性极小,所以我不想处理error了,将error返回值用“_”丢弃掉。

注意注意注意:在实际项目中,请不要丢弃error,任何意外都是可能出现的,丢弃error会导致当出现罕见的意外情况时,非常难于Debug。所有的error都应该要处理,至少写入到日志或打印到控制台。(切记,不要丢弃 error ,很多Gopher们在这个问题上有大把的血泪史)

OK,到目前为止,用Go语言搭建一个简单的网页的核心部分就完成了。

等等 .js、.css、图片怎么办?

对。例子里的模版全是HTML代码,一个漂亮的网页还必须用到图片、js脚本和css样式文件,可是...和PHP不同,请求路径是通过HandleFunc匹配到处理函数的,难道要把js、css和图片都通过函数输出后,再用HandleFunc和URL路径匹配?

处理好js、css和图片,才能做漂亮的网页,Go http静态文件的处理办法

以在index.html文件里引用一个index.js文件为例。

从代码开始

func main() {

http.HandleFunc("/", myWeb)

//指定相对路径./static 为文件服务路径

staticHandle := http.FileServer(http.Dir("./static"))

//将/js/路径下的请求匹配到 ./static/js/下

http.Handle("/js/", staticHandle)

fmt.Println("服务器即将开启,访问地址 http://localhost:8080")

err := http.ListenAndServe(":8080", nil)

if err != nil {

fmt.Println("服务器开启错误: ", err)

}

}

在项目的根目录下创建static目录,进入static目录,创建js目录,然后在js目录里创建一个index.js文件。

alert("Javascript running...");

打开之前的index.html文件,在

go语言和php哪个建站好,从0开始Go语言,用Golang搭建网站相关推荐

  1. 腾讯云自助建站CloudPages教程,不会代码小白轻松搭建网站

    腾讯云自助建站CloudPages教程,不会代码小白轻松搭建网站.腾讯云建站CloudPages自助建站模板,建站神奇不需要会代码小白轻松搭建网站,CloudPages支持海量精美建站模板,可用于搭建 ...

  2. linux 批量站群建站软件,365建站器9.0(批量建站站群软件)8月1日正式发布

    原标题:365建站器9.0(批量建站站群软件)8月1日正式发布 一.数据采集功能更新. 1.升级数据采集功能,完善365建站器数据处理能力. 升级数据采集功能,可以多个采集规则批量采集,也可以定时批量 ...

  3. [代码审计]齐博建站系统x1.0企业版代码审计

    文章目录 写在前面 齐博建站系统x1.0后台存在命令执行漏洞 齐博建站系统x1.0企业版前台反序列化漏洞 写在前面 复现2021 DASCTF July X CBCTF 4th赛题 齐博建站系统x1. ...

  4. MIPCMS建站系统 v5.0.1免费下载

    MIP 百度是非常喜欢的,基本可以做到秒收.演示站各自可以SITE.看看效果和权重. 建站系统是一套免费开源的CMS建站系统,移动网站使用MIP标准的网页,能快速让你的网站被百度收录,MIPCMS所有 ...

  5. 宝塔批量建站工具 v1.0使用图解

    功能模块简介 1,批量建站--可选创建FTP,Mysql数据库,自动备份分站账号到本地 2,批量解压 3,批量删除--删除文件,删除目录,删除站点 4,伪静态批量写入 5,批量修改 6,批量配置数据库 ...

  6. Jn建站系统2.0源码 附视频安装教程

    简介: 目前已集成的网站: 1.网() 2.代网(无需授权) 3. 博客网(自带模板) 4.易(稳定版) 5.个人导航网(简洁) 6.代理查询网 7.留言网 8.匿名网 9.表白墙() 10.抽奖网 ...

  7. java cms建站系统源码_Rongcheng CMS 融成Java后台网站内容管理系统 v3.2.1

    融成Java后台网站内容管理系统是一款基于Java语言开发的功能强大的内容管理系统.成功实现了既能够管理包括企业官网.门户站点.图片视频软件等上传下载网站.博客网站.电商购物网站.物流管理网站等复杂多 ...

  8. c语言和python哪个自学好-自学编程应该从c语言还是python入手?

    我不建议你从"基础"开始,尤其是什么:C语言,数据结构,离散数学--之类的,那是把你逼疯的节奏(感觉这是知乎学院派的风格).如果你自觉是神人级别的,当我没说. 其实软件工程发展到今 ...

  9. 【云海轻站可视化DIY建站系统V1.0.28】功能模块+可视化编辑建站系统+商用多开版+插件+公众号

    源码更新日志: [重点]文章列表组件支持更多自定义设置 [插件]每日一文新增独立入口 [优化]普通操作员不显示服务器信息 [优化]优化首次进入页面速度 [修复]表单的身份证校验中小写x不支持的问题 [ ...

最新文章

  1. Python快速学习09: 函数的参数
  2. ICLR 2022入选论文线上分享预告:一作解读,不容错过
  3. bzoj 2342: 双倍回文 回文自动机
  4. 忘记 mysql 密码的取回方法
  5. 前端学习(2848):鼠标点击事件
  6. thinkPHP5.0数据查询表达式生成技巧
  7. [CareerCup][Google Interview] 找出最小排序次数
  8. 智能跳过节假日算法java_Quartz 定时任务使用 —— 排除指定节假日时间执行任务(十一)...
  9. nodejs pm2教程(转载)
  10. 气象数据源-要素、数据集、空间分辨率、网址
  11. 宅男福利——在控制台上跳极乐净土(音频版)
  12. word宏的使用——Selection对象
  13. Adobe Photoshop CC 2014图文永久安装教程 1
  14. FC协议监控卡(FC协议分析仪),FC Monitor
  15. UI设计师这个行业到底是做什么的?
  16. 做教育怎么引流?教育行业怎么引流?培训机构引流如何转化?
  17. 【运筹学】模型速览及推荐学习视频
  18. 明明价格下降了,为什么你却花了更多钱?
  19. 用HTML/CSS制作个人简历
  20. 【NIO】Socket 编程:基于NIO的Server、Client 示例

热门文章

  1. Golang bytes.Buffer 用法精述
  2. 创建型设计模式(1)—— 单例模式(Singleton Pattern)
  3. 机器学习工程师 - Udacity 强化学习 Part Nine
  4. C++ Primer 第十三章 拷贝控制
  5. Golang错误和异常处理的正确姿势
  6. XML通过XSL格式化的那点事(XML到自定义节点折叠显示)
  7. Handler: Service中使用Toast
  8. AspxGridView 主子表设置
  9. WordPress Platinum SEO插件跨站脚本漏洞
  10. 在线VLOOKUP数据查找工具