2

第五章的示例 正是我想做的一个东西,所以我会慢慢将该示例丰富。而且该示例由浅入深,层层递进,写得很不错,特别适合学习。
还没有全部完成,就已经遇到了几个问题。希望可以解答。

  1. 在没有使用html模版之前,html相关的代码如果不添加<html></html>的标签,是不会解析html的。但是同样的代码不包含<html></html>标签,单独写在1.html文件里面用浏览器打开又是可以的,是因为go的html函数解析必需要html标签吗?
  2. 在listHandler方法里面,_,fileInfo := range fileInfoArr 获取到了文件的相关信息,但是使用imgid := fileInfo.Name 出现了一个问题,该如何使用。 method fileInfo.Name is not an expression, must be called
  3. 另外几个小问题都是印刷问题在viewHandler里面的imageId和imagePath 都没有定义 改成:=就可以了。
  4. 还有一个问题,因为这里省略了缩略图(不是本章的重点),而想实现该功能,自己在网上找一了个resize.go https://github.com/nfnt/resize 不知道这个怎么样,实际操作处理缩略图的速度很慢。还没有仔细研究,有没有比较成熟的代码和建议。

继上次提出的几个问题后:

  1. 在模版缓存这里开始,定义了一个templates,这个是全局的一个变量,定义在函数体之外,是不能使用:=的短格式,可能是copy的原因导致的,需要使用var定义。var templates = map[string]*template.Template{} 和var templates = make(map[string]*template.Template) 这两个的定义都是可以的 我想问的是他们有区别吗?
  2. 再有个问题是 templatesp[tmpl]=t 这里并没有tmpl这个变量 我想是截取代码时候造成的,但并没有测试代码,tmpl应该是upload 或者list,所以对templateName进行了split操作。
  3. 使用了check函数后,代码相应的变化,但有些地方忘了添加check有些地方err:= 不再需要:号 因为没有if所以没有了局部变量。
  4. safeHandler函数里面http.Error( w,err.Error(),http.StatusInternalServerError ) err要么改要么其它地方改 没有统一
  5. 在动静分离的时候定义了一个常量ListDir=0x001,这个十六进制的数与flags进行位运算没有太明白原理,书中因为简略的描述,没有太明白,能麻烦详细解释下吗?
  6. 在查看图片的时候 会打印 2012/10/26 17:54:14 http: StatusNotModified response with header "Content-Type" defined 是因为这个设置报出来的吗? w.Header().Set("Content-Type","image") ,另外image写得是否正确?
  7. 最后,整个示例的层层递进关系非常好,非常感谢。但示例最后好像因为截取copy等关系并没有严格的测试,还有一些格式等小问题,我将代码放置在了我的blog上面,有时间会整理所有章节的问题和示例代码进行打包发布。关于这章的http://www.ohlinux.com/archives/825/ 

这章的示例还在完善,做个小相册玩玩。

2个回答

3

已采纳

Q1: html 标签渲染和 golang 函数没有关系,用 <html> 标签包裹HTML <body> 是 html 的语法要求。书中的示例代码是简写形式,用代码输出html body,目的是为了便于过渡理解后续模板使用。

考虑到读者不可能全是做过web开发碰过html的,这里略微讲下 html 基础。

纯粹用 <html> 标签包裹起来,不同的浏览器理解行为也不一样的。

严格意义上的HTML语法,在 <html> 标签之上,也即更外部一层,需要声明 Doctype tag,而 doctype 又分多种类型,比如:

XHTML DOCTYPES

doctype html

<!DOCTYPE html>

doctype 5

<!DOCTYPE html>

doctype 1.1

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
      "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

doctype strict

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

doctype frameset

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">

doctype mobile

<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN"
      "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">

doctype basic

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN"
      "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">

doctype transitional

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

HTML 4 DOCTYPES

doctype strict

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
      "http://www.w3.org/TR/html4/strict.dtd">

doctype frameset

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN"
      "http://www.w3.org/TR/html4/frameset.dtd">

doctype transitional

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
      "http://www.w3.org/TR/html4/loose.dtd">

针对 PC 端的桌面浏览器,现在用的最多的就是 doctype 1.1 标准

doctype 1.1

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" 
      "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

加上现在 HTML5 这波热潮,doctype 5 用的也比较流行。

doctype 5

<!DOCTYPE html>

其中,doctype 1.1 标准最通用,各浏览器都能良好理解。但 doctype 5 只对支持HTML5特性的浏览器有较好的支持。

再回到正题,书里边的样例代码部分是示范性的,比如下面的 “代码片段1” 作为一个网页雏形,目的是为了便于过渡理解 “代码片段2”中的模板使用,如果楼主顺着章节看下去不难理解这是一种循序渐进的讲解。因为看到章节的末尾会有完整的工程源代码,也有标准的HTML模板文件,最终实现了一个精简的MVC 模型,可以像个正常的网站程序跑起来。

// 代码片段1

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        io.WriteString(w, "<form method=\"POST\" action=\"/upload\" "+
            " enctype=\"multipart/form-data\">"+
            "Choose an image to upload: <input name=\"image\" type=\"file\" />"+
            "<input type=\"submit\" value=\"Upload\" />"+
            "</form>")
        return
    }
    // ...
}

// 代码片段2

func uploadHandler(w http.ResponseWriter, r *http.Request) {
        if r.Method == "GET" {
            t, err := template.ParseFiles("upload.html")
            if err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
                return
            }
            t.Execute(w, nil)
            return
        }
        // ...
    }

// 按照楼主要求,加上 html 标签,代码片段1应该如下写,浏览器可以渲染成网页

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        io.WriteString(w, "<!DOCTYPE html><html><body>"+
            "<form method=\"POST\" action=\"/upload\" "+
            " enctype=\"multipart/form-data\">"+
            "Choose an image to upload: <input name=\"image\" type=\"file\" />"+
            "<input type=\"submit\" value=\"Upload\" />"+
            "</form>"+
            "</body></html>")
        return
    }
    // ...
}

《Go语言编程》是一本不厚很简洁的书,本身是因为Go简洁高效的表达,书中的内容更多的是展现Go语言如何编程及其实践使用。“用Go语言开发网站” 只是书中[网络编程]这章的一小节内容,以简短的篇幅展现了用原生的Go语言而不是借助任何第三方框架或者库来开发一个网站的具体细节,相信有过web开发基础的朋友不难理解。不过作者没有把普及 HTML 基础知识写在本书内。

Q2 and Q3: 不确定是不是排版印刷问题,我这里的原稿源代码显示如下,请自行更正:

Q2 代码片段

func listHandler(w http.ResponseWriter, r *http.Request) {
	fileInfoArr, err := ioutil.ReadDir("./uploads")
	check(err)
	locals := make(map[string]interface{})
	images := []string{}
	for _, fileInfo := range fileInfoArr {
		images = append(images, fileInfo.Name())
	}
	locals["images"] = images
	renderHtml(w, "list", locals)
}

Q3 代码片段

func viewHandler(w http.ResponseWriter, r *http.Request) {
	imageId := r.FormValue("id")
	imagePath := UPLOAD_DIR + "/" + imageId
	if ok := isExists(imagePath); !ok {
		http.NotFound(w, r)
		return
	}

	w.Header().Set("Content-Type", "image")
	http.ServeFile(w, r, imagePath)
}

书中的代码错误我们会和出版社联系,新的印刷中避免出现。

Q4: Go语言关于处理图像的原生库目前不太成熟,我们也试验过Go的图像处理最后放弃,生产环境中没有使用。成熟的图像处理工具可以参考 ImageMagick 或者 GraphicsMagick,也可以使用 七牛云存储提供的在线图像处理接口

以下内容作为楼主修改问题后的补充回答。

1.在模版缓存这里开始,定义了一个templates,这个是全局的一个变量,定义在函数体之外,是不能使用:=的短格式,可能是copy的原因导致的,需要使用var定义。var templates = map[string]*template.Template{} 和var templates = make(map[string]*template.Template) 这两个的定义都是可以的 我想问的是他们有区别吗?

templates 变量就是全局的。并非局部定义,为确定我心中的疑惑,我刚才看了下我们这边的电子书稿,是使用如下方式声明的。

var templates = make(map[string]*template.Template)

然后,我自己看书翻阅,发现代码确实是使用 := 形式声明在函数体之外的,囧。我表示找不出合适理由解释,但可以承诺会和出版社沟通书中存在样例代码错误的问题。楼主可以私信给我你的收货地址,更新后的书我买本寄送给你。

以下两段代码是等价的。

var templates = make(map[string]*template.Template)
var templates = map[string]*template.Template{}

两者的共同之处在于:返回“该map结构”的初始化值。
不同之处在于:make() 可用于创建 slices, maps, channels。比如创建 slice 时,完整的语法规格如下:

s := make([]<Type>, len, cap)

关于 make() 的细节,可以查阅书中相应的基础章节。

2.再有个问题是 templatesp[tmpl]=t 这里并没有tmpl这个变量 我想是截取代码时候造成的,但并没有测试代码,tmpl应该是upload 或者list,所以对templateName进行了split操作。

为了确认书中是否存在印刷错误,我又看了遍书。tmpl 这个变量我在书中(P150, P151)明明有看到,代码摘抄如下:

P150

func init() {
    for _, tmpl := range []string{"upload", "list"} {
        t := template.Must(template.ParseFiles(tmpl + ".html"))
        templates[tmpl] = t
    }
}

如上这段代码中的 tmpl 来自 for 循环 range 遍历 string 数组得的的元素。

range 是 Go 语言中的保留关键字,可用于循环。range 可以针对 slice、array、string、map 和 channel 类型的数据结构进行迭代操作。基于不同的内容,range 返回不同的东西,基本来讲,会从它循环的内容中返回一个 key/value 。如上代码中,当对 []string 数组(array)做循环时,range 返回序号作为 key(我们用匿名变量下划线_作丢弃处理),而这个序号所对应的内容(value)作为值赋值给了 tmpl

关于 range 保留关键字的更多细节,可以查阅书中的基础章节。

P151

func renderHtml(w http.ResponseWriter, tmpl string, locals map[string]interface{}) (err error){
        tmpl += ".html"
        err = templates[tmpl].Execute(w, locals)
}

这段代码中的 tmpl 也不是不存在,可以明显看到是从函数 renderHtml() 作为参数传入。

3.使用了check函数后,代码相应的变化,但有些地方忘了添加check有些地方err:= 不再需要:号 因为没有if所以没有了局部变量。

章节前半部分由于展示关键代码部分可能会少写代码片段,看了下书中章节中的最终代码 photoweb.go,凡是有 err 赋值的地方都紧跟着出现了 check(err) 检测。isExists() 函数中未出现 check(err) 是因为该函数 return os.IsExist(err)。另需要说明的是:check(err) 表达的是 “把程序错误终止在它该出现的地方” 这样一种思想。实际生产环境中,类似的错误处理可能需要更细致而不是那么笼统。

有些地方err:= 不再需要:号 因为没有if所以没有了局部变量。

没有看明白这句话的意思,因为没有上下文可以参考所以实在想不出来楼主要表达的意思。

一个 comma,ok 形式的语句,只要是有新变量赋值操作,那么必须用 :=,比如:

newVar, err := ...

而当 err 变量已经声明后,在没有新变量赋值操作的情况下,不必用 :=,比如书中样例代码的 uploadHandler() 函数,前两个 err 赋值语句都用了 := 操作,而第3个 err 由于没有产生新变量(_, err = ...)所有没有用 := 而是直接 = 赋值。摘抄代码如下:

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "GET" {
        renderHtml(w, "upload", nil)
    }
    if r.Method == "POST" {
        f, h, err := r.FormFile("image")
        check(err)
        filename := h.Filename
        defer f.Close()
        t, err := os.Create(UPLOAD_DIR + "/" + filename)
        check(err)
        defer t.Close()
        _, err = io.Copy(t, f)
        check(err)
        http.Redirect(w, r, "/view?id="+filename, http.StatusFound)
    }
}
4.safeHandler函数里面http.Error( w,err.Error(),http.StatusInternalServerError ) err要么改要么其它地方改 没有统一

可能是楼主好心建议,不过没有上下文参考表示没有看明白。尽管没看明白,还是稍作简单理解,应该是章节前半部分的代码片段用到了这样的写法。建议以章节中最终的 photoweb.go 程序为完整示例代码参考。完整示例代码 photoweb.go 中,由于重构关系,只有一个地方也即 safeHandler() 函数中出现 http.Error(w, e.Error(), http.StatusInternalServerError)

5.在动静分离的时候定义了一个常量ListDir=0x001,这个十六进制的数与flags进行位运算没有太明白原理,书中因为简略的描述,没有太明白,能麻烦详细解释下吗?

看了下,书中的此处代码显得有些多余;又看了下电子稿,发现跟书上确实有不一致的地方,新版印刷书中会修复此问题。此处的位运算逻辑只是判断要不要列出目录,flags 为 0 即表示只handle目录中的文件而不列目录。正确的代码如下:

func staticDirHandler(mux *http.ServeMux, prefix string, staticDir string, flags int) {
    mux.HandleFunc(prefix, func(w http.ResponseWriter, r *http.Request) {
        file := staticDir + r.URL.Path[len(prefix)-1:]
        if (flags & ListDir) == 0 {
            fi, err := os.Stat(file)
            if err != nil || fi.IsDir() {
                http.NotFound(w, r)
                return
            }
        }
        http.ServeFile(w, r, file)
    })
}
6.在查看图片的时候 会打印 2012/10/26 17:54:14 http: StatusNotModified response with header "Content-Type" defined 是因为这个设置报出来的吗? w.Header().Set("Content-Type","image") ,另外image写得是否正确?

w.Header().Set("Content-Type","image") 是作者偷懒简写,但也是可以正常work的,你问题中报的日志跟这个简写没有关系。实际应用中应该输出文件具体的 MIME-Type 类型。比如 image/jpg, image/png 等等。MIME-Type 规格如下:

Content-Type: [type]/[subtype]; parameter

MIME根据type制定了默认的subtype,当客户端不能确定消息的subtype的情况下,消息被看作默认的subtype进行处理。所以,示例程序运行时浏览器可正常显示图片。

7.最后,整个示例的层层递进关系非常好,非常感谢。但示例最后好像因为截取copy等关系并没有严格的测试,还有一些格式等小问题,我将代码放置在了我的blog上面,有时间会整理所有章节的问题和示例代码进行打包发布。关于这章的http://www.ohlinux.com/archives/825/

非常感谢楼主的热心!:)

不过书中由于篇幅只是写了一个很小很小的web例子,便于读者们理解用原生的Go语言开发web程序所需了解的各个细节。楼主若有兴趣可以写个更实用型的完整web案例!

《Go语言编程》一书的源代码在 Github 上可以找到:https://github.com/qiniu/gobook
photoweb.go 的源代码在此:https://github.com/qiniu/gobook/blob/...

欢迎提交 issues 和 pull requests!

另对书中出现的一些印刷问题,笔者表示非常抱歉,必赠楼主新书一本。

1

关于Q1的回答,我明白,是因为层层递进的编排,也明白html格式的完整性,我的问题只是比较奇怪,如果在不使用模版直接使用书中代码的不加<html>标签的情况下 浏览器是直接打印<form method="POST" action="/upload" enctype="multipart/form-data">Choose an image to upload:<input name="image" type="file" /><input type="submit" value="Upload" /></form> 并没有解析html的代码,但把这段一样的代码copy到1.html里面用浏览器打开是可以解析的。

Ajian · 2012年10月18日

1
回复 Ajian

示例Go代码执行output没有制定输出的Content-Type,浏览器缺省当文本打印了。可以在Go代码中明确输出 Content-Type: text/html 试试。磁盘上保存1.html 直接用浏览器打开,我想是浏览器程序会根据本地文件后缀名判断资源类型。

404_129743 · 2012年10月18日

展开评论
0

// 这是我最后调通的代码,仅供后来人参考
package main

import (

"html/template"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"path"

)

const (

UPLOAD_DIR   = "./uploads"
TEMPLATE_DIR = "./templates"

)

var templates = make(map[string]*template.Template)

func init() {

fileInfoArr, err := ioutil.ReadDir(TEMPLATE_DIR)
if err != nil {
    panic(err)
    return
}

var templateName, templatePath string
for _, fileInfo := range fileInfoArr {
    templateName = fileInfo.Name()
    if ext := path.Ext(templateName); ext != ".html" {
        continue
    }
    templatePath = TEMPLATE_DIR + "/" + templateName
    log.Println("Loading template:", templatePath)
    t := template.Must(template.ParseFiles(templatePath))
    templates[templateName] = t
}

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

if r.Method == "GET" {
    err := renderHtml(w, "upload.html", nil)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    return
}
if r.Method == "POST" {
    f, h, err := r.FormFile("image")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)

        return
    }
    filename := h.Filename
    defer f.Close()
    t, err := os.Create(UPLOAD_DIR + "/" + filename)
    if err != nil {
        http.Error(w, err.Error(),
            http.StatusInternalServerError)
        return
    }
    defer t.Close()
    if _, err := io.Copy(t, f); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)

        return
    }
    http.Redirect(w, r, "/view?id="+filename, http.StatusFound)
}

}

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

imageId := r.FormValue("id")
imagePath := UPLOAD_DIR + "/" + imageId
if exists := isExists(imagePath); !exists {
    http.NotFound(w, r)
    return
}
w.Header().Set("content-Type", "image")
http.ServeFile(w, r, imagePath)

}

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

fileInfoArr, err := ioutil.ReadDir("./uploads")
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}
locals := make(map[string]interface{})
images := []string{}
for _, fileInfo := range fileInfoArr {
    images = append(images, fileInfo.Name())
}
locals["images"] = images
err = renderHtml(w, "list.html", locals)
if err != nil {
    http.Error(w, err.Error(), http.StatusInternalServerError)
    return
}

}

func renderHtml(w http.ResponseWriter, tmpl string, locals map[string]interface{}) (err error) {

err = templates[tmpl].Execute(w, locals)
return

}

func isExists(path string) bool {

_, err := os.Stat(path)
if err == nil {
    return true
}
return os.IsExist(err)

}

func main() {

http.HandleFunc("/list", listHandler)
http.HandleFunc("/view", viewHandler)
http.HandleFunc("/upload", uploadHandler)
err := http.ListenAndServe(":8080", nil)
if err != nil {
    log.Fatal("ListenAndServer:", err.Error())
}

}

撰写答案