《Golang 工具 go doc 使用透析》

用户头像
卓丁
关注
发布于: 2020 年 06 月 13 日
《Golang工具go doc使用透析》

本文主要包括2个方面

  • go doc工具的基本使用。

  • 如何利用go doc工具高效阅读与分析Golang源码。

  • 其他。



go doc 工具使用介绍

日常开发中,我们可能会随时查看Golang源码或文档。主要是以下几方面的需要:

  • 开发中有用到某个类型、函数或包,需要查看文档或源码深入学习其特性,以更好的使用。

  • 业余钻研的需要,想深入的学习某个包、函数、或类型等的源码。

不管哪种需要,如能方便快捷的查阅到官方相关文档或源码,那自然给咱节省不少时间。不过可能因网络原因有时不能及时访问到golang官网去查看权威文档。额外,每个开发者喜好不同,有部分开发者是vim的重度依赖者,不管是写代码还是看文档,都不想离开控制台。

在文档方面,go官方提供了太多宝贵的资源供我们学习参考。 除此之外,go还提供了一个非常棒的命令行小工具,它就是go doc。在介绍go doc之前,我们需要先简单回忆下go常用环境变量作为铺垫。

比如我们在安装好go环境以后,可以运行go env 查看一下当前Golang环境变量的基本情况。

比如,下面是我机器上安装Golang后的基本环境变量一览:

➜ ~ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/jordy/Library/Caches/go-build"
GOENV="/Users/jordy/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY="git.xxx.com"
GONOSUMDB="git.xxx.com"
GOOS="darwin"
GOPATH="/Users/jordy/go"
GOPRIVATE="git.xxx.com"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/Cellar/go/1.13.8/libexec"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.13.8/libexec/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
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/66/vyyywgr55f1b7shr11s8hg5w0000gn/T/go-build695446787=/tmp/go-build -gno-record-gcc-switches -fno-common"

其中,有2个环境变量是必须要掌握:一个是GOROOT ,另一个是GOPATH

GOROOT 表示Golang安装的根目录,比如我的是"/usr/local/Cellar/go/1.13.8/libexec"

GOPATH 表示项目工程的根目录,比如我的是 "/Users/jordy/go"

当然Golang比较重要的环境变量不止这2个,但本文我们介绍的go doc将主要依赖于GOROOTGOPATH



关于go doc 工具的具体介绍可以查看帮助文档

➜ ~ go help doc
usage: go doc [-u] [-c] [package|[package.]symbol[.methodOrField]]

Doc prints the documentation comments associated with the item identified by its
arguments (a package, const, func, type, var, method, or struct field)
followed by a one-line summary of each of the first-level items "under"
that item (package-level declarations for a package, methods for a type,
etc.).



接下来,我们先简单体验下它的用途;

比如我们想查看某个包的文档,可以直接在控制台运行go doc 并跟上要查阅的包名,

举例,比如我们想查看url这个包(该包属于net包),可以直接运行 go doc url

➜ ~ go doc url
package url // import "net/url"
Package url parses URLs and implements query escaping.
func PathEscape(s string) string
func PathUnescape(s string) (string, error)
func QueryEscape(s string) string
func QueryUnescape(s string) (string, error)
type Error struct{ ... }
type EscapeError string
type InvalidHostError string
type URL struct{ ... }
func Parse(rawurl string) (*URL, error)
func ParseRequestURI(rawurl string) (*URL, error)
type Userinfo struct{ ... }
func User(username string) *Userinfo
func UserPassword(username, password string) *Userinfo
type Values map[string][]string
func ParseQuery(query string) (Values, error)

你看多方便呀,控制台直接帮我们实时输出了对应的文档。我们看到url包中有一个URL的结构体类型,如果我们想进一步查看这个URL结构体的细节,如类型定义、所包含的属性,与它有关的方法或函数等,则可以直接运行go doc url.URL查看 ,示例如下:

go doc url.URL
package url // import "net/url"
type URL struct {
Scheme string
Opaque string // encoded opaque data
User *Userinfo // username and password information
Host string // host or host:port
Path string // path (relative paths may omit leading slash)
RawPath string // encoded path hint (see EscapedPath method)
ForceQuery bool // append a query ('?') even if RawQuery is empty
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
}
A URL represents a parsed URL (technically, a URI reference).
The general form represented is:
[scheme:][//[userinfo@]host][/]path[?query][#fragment]
URLs that do not start with a slash after the scheme are interpreted as:
scheme:opaque[?query][#fragment]
Note that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.
A consequence is that it is impossible to tell which slashes in the Path
were slashes in the raw URL and which were %2f. This distinction is rarely
important, but when it is, the code should use RawPath, an optional field
which only gets set if the default encoding is different from Path.
URL's String method uses the EscapedPath method to obtain the path. See the
EscapedPath method for more details.
func Parse(rawurl string) (*URL, error)
func ParseRequestURI(rawurl string) (*URL, error)
func (u *URL) EscapedPath() string
func (u *URL) Hostname() string
func (u *URL) IsAbs() bool
func (u *URL) MarshalBinary() (text []byte, err error)
func (u *URL) Parse(ref string) (*URL, error)
func (u *URL) Port() string
func (u *URL) Query() Values
func (u *URL) RequestURI() string
func (u *URL) ResolveReference(ref *URL) *URL
func (u *URL) String() string
func (u *URL) UnmarshalBinary(text []byte) error



如果我们想进一步查看属于url.URL类型的具体某个方法呢?

同样地,直接在类型后面跟上方法名称即可,如go doc url.URL.EscapedPath,示例见下:

➜ ~ go doc url.URL.EscapedPath
package url // import "net/url"

func (u *URL) EscapedPath() string
EscapedPath returns the escaped form of u.Path. In general there are
multiple possible escaped forms of any path. EscapedPath returns u.RawPath
when it is a valid escaping of u.Path. Otherwise EscapedPath ignores
u.RawPath and computes an escaped form on its own. The String and RequestURI
methods use EscapedPath to construct their results. In general, code should
call EscapedPath instead of reading u.RawPath directly.

到此,我想各位已经感受到了go doc的简单以及便利之处吧。有人说,我不喜好用命令行之类的工具查看文档,而是更习惯用浏览器查看,但官网有时又难以访问或响应慢,关键时候比较闹心。没事,go doc 也可以帮你实现在浏览器中打开一个页面来查看文档 ;浏览器查看文档?文档哪里来的?别着急,咱们慢慢来介绍。

我们可以使用和go doc 命令类似的godoc命令(比如go和doc中间无空格!),该工具原理跟go doc 命令一样,并支持以下参数项:

➜ ~ godoc --help
usage: godoc -http=localhost:6060
-analysis string
comma-separated list of analyses to perform (supported: type, pointer). See http://golang.org/lib/godoc/analysis/help.html
-goroot string
Go root directory (default "/usr/local/Cellar/go/1.13.8/libexec")
-http string
HTTP service address (default "localhost:6060")
-index
enable search index
-index_files string
glob pattern specifying index files; if not empty, the index is read from these files in sorted order
-index_interval duration
interval of indexing; 0 for default (5m), negative to only index once at startup
-index_throttle float
index throttle value; 0.0 = no time allocated, 1.0 = full throttle (default 0.75)
-links
link identifiers to their declarations (default true)
-maxresults int
maximum number of full text search results shown (default 10000)
-notes string
regular expression matching note markers to show (default "BUG")
-play
enable playground
-templates string
load templates/JS/CSS from disk in this directory
-timestamps
show timestamps with directory listings
-url string
print HTML for named URL
-v verbose mode
-write_index
write index to a file; the file name must be specified with -index_files
-zip string
zip file providing the file system to serve; disabled if empty



使用godoc 就可以帮我们把刚才命令行所示的文档在浏览器直接打开,具体如何操作,效果如何呢?

很简单,godoc 有一个-http参数项,它可以帮我们在本地启动一个监听端口(如localhost:6060),将刚才的文档在浏览器输出,如我们运行 godoc -http :6060

godoc -http :6060

这时,我们在浏览器中访问http://localhost:6060,天呐,奇迹出现了:



godoc 竟然帮我们“做”了一个跟go官网类似的本地“官网”,我们想查看文档,可以点击Document ,进入文档页,如下图



我们想查看具体的某个包相关文档,可以直接点击Packages ,或者直接访问具体的包,如下图:

http://localhost:6060/pkg/net/url/#example_URL_EscapedPath



怎么样,简直亮瞎眼哈!

不管是控制台查看,还是浏览器查看,go doc(godoc)是如何帮我们输出如此精美的文档的呢? 答案就是我们刚刚所提到环境变量,GOROOT GOPATH 等。原来,godoc会先在GOROOT 环境变量所在目录下的src目录中搜索我们要查阅对包或关键字,如果未找到, godoc会继续在GOPATH环境变量所在目录下帮我们查找并输出,GOROOT目录结构举例如下,其中源码在子目录src中:

➜ pwd
/usr/local/Cellar/go/1.13.8/libexec
➜ tree -L 1
.
├── CONTRIBUTING.md
├── CONTRIBUTORS
├── PATENTS
├── SECURITY.md
├── VERSION
├── api
├── bin
├── doc
├── favicon.ico
├── lib
├── misc
├── pkg
├── robots.txt
├── src
└── test

8 directories, 7 files



GOPATH目录如

➜ go tree -L 1
.
├── bin
├── pkg
└── src



通过以上的学习,咱们已经掌握了如何利用godoc在日常开发中快速查阅文档,同时也明白了godoc如此便利的原因。遗留一个小问题供大家尝试:如何用godoc将自己写的代码生成对应的文档?

接下来,咱们抛开工具本身,举个小例子来简单体验一下阅读与分析Golang源码的乐趣。

源码一:golang类如何“实现”某个接口?



我们知道在面向对象编程的思想中,我们可以预先定义一组接口,其中包含一些抽象化的行为;然后定义若干实现了该接口的类,在类中可以将接口的行为方法做具体的实现;

其实,接口本质上就是一个统一的约定,一种抽象的类型;而类如果实现某个接口,则是遵守了该接口的行为和准则;

这样以来,不用将接口定义和具体实现绑定在一起,编码就相对比较灵活,也比较容易统一化的扩展共性的行为;

比如,在具有代表性的面向对象编程语言C++、Java中,就是使用抽象类或interface实现了抽象化的定义,类可以通过关键字显式地指定implements实现某个接口;

同样地,在Go语言中也有接口,我们也可以预先定义一组接口作为约定。但在对接口的实现上,golang则有所不同;

在Java或C++中,一个具体的类要实现一个抽象类或者interface时,需要明确的指定谁实现谁,比如:

public interface Transportation
void fly()
public class Plane implements Fly
public void fly()
System.out.println("Plane fly"); 
}



#include <iostream>
using namespace std;
// 基类
class Shape
{
public:
// 提供接口框架的纯虚函数
virtual int getArea() = 0;
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
protected:
int width;
int height;
};
// 派生类
class Rectangle: public Shape
{
public:
int getArea()
{
return (width * height);
}
};
class Triangle: public Shape
{
public:
int getArea()
{
return (width * height)/2;
}
};
int main(void)
{
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
// 输出对象的面积
cout << "Total Rectangle area: " << Rect.getArea() << endl;
Tri.setWidth(5);
Tri.setHeight(7);
// 输出对象的面积
cout << "Total Triangle area: " << Tri.getArea() << endl;
return 0;
}



在Golang中,只要一个类(用一个结构体来定义)实现了某个接口interface的所有方法,就认为该类实现了该接口!所有将此接口作为参数的地方,我们都可以传递该类作为参数;额外,也可以将该类的实例赋值给该接口。

下面我们在源码中找一个例子做分析:

➜  src go doc fmt.Fprintf
package fmt // import "fmt"
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
    Fprintf formats according to a format specifier and writes to w. It returns
    the number of bytes written and any write error encountered.

比如在fmt包中有一个Fprintf方法,该方法的功能是可以将若干变量格式化后输出到我们指定的输出句柄中(比如标准输出浏览器等),它有3个参数,分别是:

  • io.Writer 定义输出句柄,如可以是标准输出,

  • format 定义格式化参数

  • a 被输出的若干变量

Fprintf-test.go

1 package main
2
3 import (
4 "fmt"
5 "os"
6 )
7
8 func main() {
9 const name, age = "Kim", 22
10 n, err := fmt.Fprintf(os.Stdout, "%s is %d years old.\n", name, age)
11
12 // The n and err return values from Fprintf are
13 // those returned by the underlying io.Writer.
14 if err != nil {
15 fmt.Fprintf(os.Stderr, "Fprintf: %v\n", err)
16 }
17 fmt.Printf("%d bytes written.\n", n)
18
19 }



运行以上示例,输出:

➜  src go run /tmp/Fprintf.go

Kim is 22 years old.

21 bytes written.



我们看到,在第10行,我们给第一个参数(输出句柄)传递了os.Stdout来表示输出到标准输出。

下面我们就结合源码来深入分析一下第一个参数指定的类型io.Writer:

先查看它的定义,Writer是一个接口,该接口定义类一个基本的方法Write:

➜  src go doc io.Writer

package io // import "io"

type Writer interface {

Write(p []byte) (n int, err error)

}

    Writer is the interface that wraps the basic Write method.

    Write writes len(p) bytes from p to the underlying data stream. It returns

    the number of bytes written from p (0 <= n <= len(p)) and any error

    encountered that caused the write to stop early. Write must return a non-nil

    error if it returns n < len(p). Write must not modify the slice data, even

    temporarily.



io.Writer 参数的类型是一个接口,而我们在具体使用时给其赋值了类型os.Stdout。由上面学习我们得知;

Golang中,一个类型如果可以赋值给一个接口,说明该类型必是实现了该接口,那我们来看下os包中

的类型Stdout的源码定义:

➜  src go doc os.Stdout

package os // import "os"

var (

Stdin  = NewFile(uintptr(syscall.Stdin), "/dev/stdin")

Stdout = NewFile(uintptr(syscall.Stdout), "/dev/stdout")

Stderr = NewFile(uintptr(syscall.Stderr), "/dev/stderr")

)

    Stdin, Stdout, and Stderr are open Files pointing to the standard input,

    standard output, and standard error file descriptors.

    Note that the Go runtime writes to standard error for panics and crashes;

    closing Stderr may cause those messages to go elsewhere, perhaps to a file

    opened later.



看到Stdout是由调用同包的NewFile函数返回,进一步查看NewFile函数;

➜  src go doc os.NewFile

package os // import "os"

func NewFile(fd uintptr, name string) *File

    NewFile returns a new File with the given file descriptor and name. The

    returned value will be nil if fd is not a valid file descriptor. On Unix

    systems, if the file descriptor is in non-blocking mode, NewFile will

    attempt to return a pollable File (one for which the SetDeadline methods

    work).



看到NewFile函数的返回值类型是同包的File,层层递进,进一步去查看os包下File类型:

➜  src go doc os.File

package os // import "os"
type File struct {
// Has unexported fields.
}
    File represents an open file descriptor.
func Create(name string) (*File, error)
func NewFile(fd uintptr, name string) *File
func Open(name string) (*File, error)
func OpenFile(name string, flag int, perm FileMode) (*File, error)
func (f *File) Chdir() error
func (f *File) Chmod(mode FileMode) error
func (f *File) Chown(uid, gid int) error
func (f *File) Close() error
func (f *File) Fd() uintptr
func (f *File) Name() string
func (f *File) Read(b []byte) (n int, err error)
func (f *File) ReadAt(b []byte, off int64) (n int, err error)
func (f *File) Readdir(n int) ([]FileInfo, error)
func (f *File) Readdirnames(n int) (names []string, err error)
func (f *File) Seek(offset int64, whence int) (ret int64, err error)
func (f *File) SetDeadline(t time.Time) error
func (f *File) SetReadDeadline(t time.Time) error
func (f *File) SetWriteDeadline(t time.Time) error
func (f *File) Stat() (FileInfo, error)
func (f *File) Sync() error
func (f *File) SyscallConn() (syscall.RawConn, error)
func (f *File) Truncate(size int64) error
func (f *File) Write(b []byte) (n int, err error)
func (f *File) WriteAt(b []byte, off int64) (n int, err error)
func (f *File) WriteString(s string) (n int, err error)



我们看到,File类型的确实现了一个Write方法(跟Writer 接口定义的方法一样),func (f *File) Write(b []byte) (n int, err error),所以我们可以肯定的说,File类型已经继承和实现了Writer 接口;

到这里我们明白了,其实,绕了一大圈,我们传递给 fmt.Fprintf方法到第一个参数,本质上是一个实现类接口io.Writer的File类型,所以可以直接将os.Stdout传递给fmt.Fprintf方法作为第一个参数(os.Stdout is a io.Writer);

额外,刚看到os.Stdout在定义的时候,本质上是一个 File,但从字面含义上去了解该定义,刚好也高度符合Unix的精髓哈:一切皆文件;

附录



发布于: 2020 年 06 月 13 日 阅读数: 139
用户头像

卓丁

关注

鸟过无痕 2017.12.10 加入

泰戈尔:虽然天空没有留下我的痕迹,但我已飞过。

评论

发布
暂无评论
《Golang工具go doc使用透析》