GO
目标:
go开发项:
接入网测试工具:ping dns wget tracert,可以跨平台使用,包括win、mac、linux、安卓
https://smartping.org/
接入网测试工具:
1、访问局点的URL,根据服务端配置的探测条件进行探测,可以将结果上传到服务器
2、探测内容:ping dns
HIDS下载安装包:
1、现状:现在hids部署是通过ansible部署的,没有走cops这个平台,现在研发推包是会直接推送到内网的服务器上面,然后会运行rsync+lsyncd会自动同步到外网的服务器上面,然后再在外网的服务器上面做每个版本的打包。由于版本更新比较多,加上又引入了docker,没有必要再使用这个方式去进行了。加上不能上网的客户只有一个,先支持在线。
2、需求:ansible文件是通过yml写的,每个版本,只压缩这些文件,微服务的安装包通过网络下载,但是要保证安装包的安全性
3、功能:
1)服务器根据一定的规则生成一个密钥。密钥要有自动更新机制、每次调用完成后重新生成密钥
2)入参,表示需要下载的包名,all表示全部包。读取mysql_var.yml、nginx_var.yml获取版本号,将这些内容发送到服务端。
3)服务端收到请求之后,先做密钥的验证,验证通过之后更新limitip.conf且通知client
4)client认证通过之后,开始下载数据
5)下载完成,通过server删除limitip.conf的配置
参考项目:
源码包:
GO语言中文网:
https://studygolang.com/pkgdoc
GO语言菜鸟教程:
https://www.runoob.com/go/go-tutorial.html
构建GO程序
编译:build
go build test.go
编译后,将在当前路径下生成一个可执行二进制文件:Windows下生成的是test.exe文件,Unix下生成的是test文件。既然是可执行文件,当然可以直接执行:
$ ./test
将输出"Hello World"。
也可以直接通过go的run工具将编译和运行两个步骤合二为一:
$ go run test.go
Hello World
go run不会生成可执行的二进制文件,它实际上是将编译得到的文件放进一个临时目录,然后执行,执行完后自动清理临时目录。
先写两个go文件,一个是可执行go文件test.go,一个是共享库strutils.go,将它们放在workspace的src下。
$ mkdir -p $GOPATH/src/{hello,strutils}
$ tree -C
.
├── bin
├── pkg
├── src
│ ├── hello
│ │ └── test.go
│ └── strutils
│ └── strutils.go
注意,上面故意将test.go放在名为hello的目录下,可以将其放在src下的任何非库文件目录下(例如不能放进strutils目录下),名称不要求。
hello/test.go的内容如下:
package main
import (
"fmt"
"strutils"
)
func main() {
fmt.Println("Hello World")
fmt.Println(strutils.ToUpperCase("hello world"))
}
strutils/strutils.go的内容如下:
package strutils
import (
"strings"
)
func ToUpperCase(s string) string{
return strings.ToUpper(s)
}
func ToLowerCase(s string) string{
return strings.ToLower(s)
}
go build可以用于编译,编译时会对import导入的包进行搜索,搜索的路径为标准库所在路径$GOROOT/src、workspace下的src目录。它只会生成额外的可执行文件放在当前目录下,不会生成额外的库文件。但需要注意,生成的可执行文件名称可能会出乎意料:
例如进入到目录src/hello下,对test.go的文件进行编译,以下三种build路径都可用成功编译:
cd src/hello
go build # 生成的可执行文件名为hello
go build . # 生成的可执行文件名为hello
go build test.go # 生成的可执行文件名为test
前两者是等价的,当go build以目录的形式进行编译,则生成的可执行文件名为目录名。当go build以go代码文件名的方式进行编译,则生成的可执行程序名为go源码文件名(去掉后缀.go或增加后缀.exe)。
install
go还有一个工具install,go install的操作称为安装,将文件安装到合适的位置。go install时会先进行编译,然后将编译后的二进制文件保存到workspace的bin目录下,将编译后的库文件(称为包对象文件,以".a"为后缀)放在pkg目录下。
注意,go install时必须先进入到$GOPATH/src下,且只能对目录进行操作,不能对具体的go文件操作,因为go认为包和目录名相同。给go install指定一个目录名,就表示编译这个包名。
例如,对src/hello下的test.go进行安装,由于它导入了strutils包,所以会自动将strutils也安装好:
$ cd $GOPATH/src
$ go install hello
$ tree $GOPATH
/gocode
├── bin
│ └── hello # 二进制程序文件名为hello,而非test
├── pkg
│ └── linux_amd64
│ └── strutils.a # 库文件
└── src
├── hello
│ └── test.go
└── strutils
└── strutils.go
还可以单独对库文件进行安装:
$ rm -rf $GOPATH/bin/* $GOPATH/pkg/*
$ cd $GOPATH/src
$ go install strutils
/gocode
├── bin
├── pkg
│ └── linux_amd64
│ └── strutils.a
└── src
├── hello
│ └── test.go
└── strutils
└── strutils.go
如果省略目录名,则表示对当前目录下的包进行安装:
$ cd $GOPATH/src/hello
$ go install
再次提醒,go install前先进入到$GOPATH/src目录下。
由于go install可以直接安装二进制文件到$GOPATH/bin,所以出于方便执行这些二进制程序,可以将这个目录放进PATH环境变量。
$ export PATH=$PATH:`go env GOPATH`/bin
构建go程序的规范建议
1.由于可以将所有go项目放在同一个$GOPATH目录下,为了区分src下的项目目录和库文件目录,建议将每个项目目录设置深一点。
例如:
bin
pkg
src
|- /first/project
|- main.go
|- myliba
|- a.go
|- b.go
|- mylibb
|- c.go
|- d.go
|- /second/project
|- main.go
|- lib
|- a.go
|- b.go
2.go install时,先进入到项目目录下。
3.库文件的名称(也是目录名)要选取合理,尽量短,但却尽量见名知意,也尽量减少名称重复的几率。
例如util这种名称到处都是,可以修改为numutil、nameutil等。
import导包和初始化阶段
import导入包
搜索路径
import用于导入包:
import (
"fmt"
"net/http"
"mypkg"
)
编译器会根据上面指定的相对路径去搜索包然后导入,这个相对路径是从GOROOT或GOPATH(workspace)下的src下开始搜索的。
假如go的安装目录为/usr/local/go,也就是说GOROOT=/usr/local/go,而GOPATH环境变量GOPATH=~/mycode:~/mylib,那么要搜索net/http包的时候,将按照如下顺序进行搜索:
/usr/local/go/srcnet/http
~/mycode/src/net/http
~/mylib/src/net/http
以下是go install搜索不到mypkg包时的一个报错信息:
can't load package: package mypkg: cannot find package "mypkg" in any of:
/usr/lib/go-1.6/src/mypkg (from $GOROOT)
/golang/src/mypkg (from $GOPATH)
也就是说,go总是先从GOROOT出先搜索,再从GOPATH列出的路径顺序中搜索,只要一搜索到合适的包就理解停止。当搜索完了仍搜索不到包时,将报错。
包导入后,就可以使用这个包中的属性。使用包名.属性的方式即可。例如,调用fmt包中的Println函数fmt.Println。
包导入的过程

首先从main包开始,如果main包中有import语句,则会导入这些包,如果要导入的这些包又有要导入的包,则继续先导入所依赖的包。重复的包只会导入一次,就像很多包都要导入fmt包一样,但它只会导入一次。
每个被导入的包在导入之后,都会先将包的可导出函数(大写字母开头)、包变量、包常量等声明并初始化完成,然后如果这个包中定义了init()函数,则自动调用init()函数。init()函数调用完成后,才回到导入者所在的包。同理,这个导入者所在包也一样的处理逻辑,声明并初始化包变量、包常量等,再调用init()函数(如果有的话),依次类推,直到回到main包,main包也将初始化包常量、包变量、函数,然后调用init()函数,调用完init()后,调用main函数,于是开始进入主程序的执行逻辑。
别名导入和特殊的导入方法
当要导入的包重名时会如何?例如network/convert包用于转换从网络上读取的数据,file/convert包用于转换从文件中读取的数据,如果要同时导入它们,当引用的时候指定convert.FUNC(),这个convert到底是哪个包?
可以为导入的包添加一个名称属性,为包设置一个别名。例如,除了导入标准库的fmt包外,自己还定义了一个mypkg/fmt包,那么可以如下导入:
package main
import (
"fmt"
myfmt "mypkg/fmt"
)
func main() {
fmt.Println()
myfmt.myfunc() // 使用别名进行访问
}
如果不想在访问包属性的时候加上包名,则import导入的时候,可以为其设置特殊的别名:点(.)。
import (
. "fmt"
)
func main() {
Println() // 无需包名,直接访问Println
}
这时要访问fmt中的属性,必须不能使用包名fmt。
go要求import导入的包必须在后续中使用,否则会报错。如果想要避免这个错误,可以在包的前面加上下划线:
import (
"fmt"
_ "net/http"
"mypkg"
)
这样在当前包中就无需使用net/http包。其实这也是为包进行命名,只不过命名为"_",而这个符号又正好表示丢弃赋值结果,使得这成为一个匿名包。
下划线(_)
在go中,下划线出现的频率非常高,它被称为blank identifier,可以用于赋值时丢弃值,可以用于保留import时的包,还可以用于丢弃函数的返回值。详细内容可参见官方手册:https://golang.org/doc/effective_go.html#blank
导入而不使用看上去有点多此一举,但并非如此。因为导入匿名包仅仅表示无法再访问其内的属性。但导入这个匿名包的时候,会进行一些初始化操作(例如init()函数),如果这个初始化操作会影响当前包,那么这个匿名导入就是有意义的。
远程包
现在通过分布式版本控制系统进行代码共享是一种大趋势。go集成了从gti上获取远程代码的能力。
例如:
$ go get github.com/golang/example
在import语句中也可以使用,首先从GOPATH中搜索路径,显然这是一个URL路径,于是调用go get进行fetch,然后导入。
import (
"fmt"
"github.com/golang/example"
)
当需要从git上获取代码的时候,将调用go get工具自动进行fetch、build、install。如果workspace中已经有这个包,那么将只进行最后的install阶段,如果没有这个包,将保存到GOPATH的第一个路径中,并build、install。
go get是递归的,所以可以直接fetch整个代码树。
常量和变量的初始化
Go中的常量在编译期间就会创建好,即使是那些定义为函数的本地常量也如此。常量只允许是数值、字符(runes)、字符串或布尔值。
由于编译期间的限制,定义它们的表达式必须是编译器可评估的常量表达式(constant expression)。例如,1<<3是一个常量表达式,而math.Sin(math.Pi/4)则不是常量表达式,因为涉及了函数math.Sin()的调用过程,而函数调用是在运行期间进行的。
变量的初始化和常量的初始化差不多,但初始化的变量允许是"需要在执行期间计算的一般表达式"。例如:
var (
home = os.Getenv("HOME")
user = os.Getenv("USER")
gopath = os.Getenv("GOPATH")
)
init()函数
Go中除了保留了main()函数,还保留了一个init()函数,这两个函数都不能有任何参数和返回值。它们都是在特定的时候自动调用的,无需我们手动去执行。
还是这张图:

每个包中都可以定义init函数,甚至可以定义多个,但建议每个包只定义一个。每次导入包的时候,在导入完成后,且变量、常量等声明并初始化完成后,将会调用这个包中的init()函数。
对于main包,如果main包也定义了init(),那么它会在main()函数之前执行。当main包中的init()执行完之后,就会立即执行main()函数,然后进入主程序。
所以,init()经常用来初始化环境、安装包或其他需要在程序启动之前先执行的操作。如果import导入包的时候,发现前面命名为下划线_了,一般就说明所导入的这个包有init()函数,且导入的这个包除了init()函数外,没有其它作用。
Go的目录结构
在gopath模式下
作为一个workspace,需要包含src、pkg、bin三个目录
workspace(/root/code/go)
|-- src //源码目录(自己写的工程,或是go get获取的工程)
|-- 我的工程
|--vendor //可选,该工程依赖的三方包
|-- pkg //编译生成的静态包文件(一般为.a文件)
|-- bin //go可执行工具目录(执行go install后会复制到此目录下)
设置环境变量:
export GOPATH=/root/code/go
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin:$GOPATH/bin
源码:必须放在src目录下
依赖包:放在src目录下(一般通过go get获取),或是放在”我的工程“目录下自己新建的vendor目录里。
在go mod模式下
源码可以放在任意地方,go mod依赖包会先下载到pkg目录,保存为cach文件再解压,以包名+版本号进行版本区分,如grpc@v1.28.0
源码:任意位置
依赖包:告别vendor目录,通过go mod管理,会下载到pkg目录下。
go mod常用命令
- go mod init # 初始化当前目录为模块根目录,生成go.mod, go.sum文件
- go mod download # 下载依赖包
- go mod tidy #整理检查依赖,如果缺失包会下载或者引用的不需要的包会删除
- go mod vendor #复制依赖到vendor目录下面
- go mod 可看完整所有的命令
- go mod graph 以文本模式打印模块需求图
- go mod verify 验证依赖是否正确
- go mod edit 编辑go.mod文件
引用自定义包
引用自定义包实例:
这里go mod init mytest,故意将module名和目录配置不一样。
root# cat go.mod
module mytest
go 1.14
工程目录结构如下:
例1:import "mytest/mdAbc"
mytest为go mod 的module名
./mdAbc目录名和包同名
直接import "mytest/mdAbc"
例2:import "mytest/json_test/jsonmode"
mytest为go mod 的module名
./json_test包名和子目录同名,都是jsonmode,因此直接import "mytest/json_test/jsonmode"
当然也可以取别名的方式:xx "mytest/json_test/jsonmode"
例3:import hi "mytest/mdHi"
目录./mdHi和包(hi)不同名,只能以取别名的方式import
例4:import wo "mytest/mdWo"
目录./mdWo和包(wo)不同名,只能以取别名的方式import
例5:import ha "mytest/mdWo/mdHa"
目录./mdWo/mdHa和包(ha)不同名,只能以取别名的方式import
如何使用自建的go文件
go是通过import来实现,我们熟知的fmt就可以通过import "fmt"进行引用,这是引用的系统库,我们自定义的go文件也是通过import 来引用,只不过需要按照一定的规范才能够引用
下面我们来看看go是如何引用自定义的包的
创建
首先,我们自定义自己的包,名为pac.go,在工程目录下创建pac的文件夹,把pac.go放到pac文件夹中

编辑pac.go,首行package pac定义包名,如“fmt”,“main”,包名要和文件夹名字一样,需要外部调用的函数及变量等首字母需要大写,否则外部无法调用
package pac
import "fmt"
func Printstr(x string) {
fmt.Println(x + " is pac")
}
引用
package main
import "package-test/pac"
func main() {
pac.Printstr("this")
}
通过import进行引用,我们go的工程一般都是放在$GOPATH/src,import引用的路径为$GOPATH/src的相对路径

使用方式为包名.xxx,如pac.Printstr("this")
如果使用vs code,直接调用进行保存会自定添加满足的包,但是如果手动import包,但没有调用该包的内容,保存后会自动去掉该引用,有时候会误导人以为引用的路径不对导致的
包含多个文件
一个包内可以包含多个文件,即文件夹内可以有多个文件,每个文件的包名必须都一样,每个文件可直接互相引用
pac.go
package pac
import "fmt"
func Printstr(x string) {
fmt.Println(x + " is pac")
}
var test string
pac2.go
package pac
import "fmt"
func Printstr2(x string) {
test = "2222"
fmt.Println(test)
}
pac2可直接使用test,我们可以理解为同个包下内容是相通的,只是被拆分为不同的文件
如果要引用其他包,必须使用import
包
http
http.HandleFunc
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc registers the handler function for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.
template
template.ParseFiles
func ParseFiles(filenames ...string) (*Template, error)
ParseFiles creates a new Template and parses the template definitions from the named files. The returned template's name will have the (base) name and (parsed) contents of the first file. There must be at least one file. If an error occurs, parsing stops and the returned *Template is nil.
When parsing multiple files with the same name in different directories, the last one mentioned will be the one that results. For instance, ParseFiles("a/foo", "b/foo") stores "b/foo" as the template named "foo", while "a/foo" is unavailable.
func ParseFiles(filenames ...string) (*模板,错误)
ParseFiles 创建一个新模板并从命名文件中解析模板定义。 返回的模板名称将包含第一个文件的(基本)名称和(解析的)内容。 必须至少有一个文件。 如果发生错误,解析停止并且返回的 *Template 为 nil。
当解析不同目录中的多个同名文件时,最后提到的将是结果。 例如, ParseFiles("a/foo", "b/foo") 将 "b/foo" 存储为名为 "foo" 的模板,而 "a/foo" 不可用。
flag
Package flag implements command-line flag parsing.
包标志实现命令行标志解析。
Usage
Define flags using flag.String(), Bool(), Int(), etc.
This declares an integer flag, -n, stored in the pointer nFlag, with type *int:
import "flag"
var nFlag = flag.Int("n", 1234, "help message for flag n")
If you like, you can bind the flag to a variable using the Var() functions.
如果愿意,可以使用 Var() 函数将标志绑定到变量。
var flagvar int
func init() {
flag.IntVar(&flagvar, "flagname", 1234, "help message for flagname")
}
Or you can create custom flags that satisfy the Value interface (with pointer receivers) and couple them to flag parsing by
或者您可以创建满足 Value 接口(带有指针接收器)的自定义标志,并将它们耦合到标志解析
flag.Var(&flagVal, "name", "help message for flagname")
For such flags, the default value is just the initial value of the variable.
After all flags are defined, call
定义完所有标志后,调用
flag.Parse()
to parse the command line into the defined flags.
Flags may then be used directly. If you're using the flags themselves, they are all pointers; if you bind to variables, they're values.
fmt.Println("ip has value ", *ip)
fmt.Println("flagvar has value ", flagvar)
After parsing, the arguments following the flags are available as the slice flag.Args() or individually as flag.Arg(i). The arguments are indexed from 0 through flag.NArg()-1.
Command line flag syntax
The following forms are permitted:
-flag
-flag=x
-flag x // non-boolean flags only
One or two minus signs may be used; they are equivalent. The last form is not permitted for boolean flags because the meaning of the command
cmd -x *
where * is a Unix shell wildcard, will change if there is a file called 0, false, etc. You must use the -flag=false form to turn off a boolean flag.
Flag parsing stops just before the first non-flag argument ("-" is a non-flag argument) or after the terminator "--".
Integer flags accept 1234, 0664, 0x1234 and may be negative. Boolean flags may be:
1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False
Duration flags accept any input valid for time.ParseDuration.
The default set of command-line flags is controlled by top-level functions. The FlagSet type allows one to define independent sets of flags, such as to implement subcommands in a command-line interface. The methods of FlagSet are analogous to the top-level functions for the command-line flag set.
pkg.go.dev 的 flag
io/ioutil
Package ioutil implements some I/O utility functions.
包 ioutil 实现了一些 I/O 实用程序功能。
io.Writer
Writer 是封装了基本 Write 方法的接口。
Write 将 len(p) 个字节从 p 写入底层数据流。它返回
从 p (0 <= n <= len(p)) 写入的字节数和任何错误
遇到导致写入提前停止的情况。写必须返回一个非零
如果返回 n < len(p) 则出错。写入一定不能修改切片数据,即使
暂时地。
ioutil.WriteFile
Package ioutil implements some I/O utility functions.
包 ioutil 实现了一些 I/O 实用程序功能。
func WriteFile(filename string, data []byte, perm fs.FileMode) error
WriteFile writes data to a file named by filename. If the file does not
exist, WriteFile creates it with permissions perm (before umask); otherwise
WriteFile truncates it before writing, without changing permissions.
func WriteFile(filename string, data []byte, perm fs.FileMode) 错误
WriteFile 将数据写入由文件名命名的文件。 如果文件没有
存在,WriteFile 使用权限 perm 创建它(在 umask 之前); 除此以外
WriteFile 在写入之前将其截断,而不更改权限。
Go Web: 处理请求
Request和Response
http Requset和Response的内容包括以下几项:
- Request or response line
- Zero or more headers
- An empty line, followed by …
- … an optional message body
例如一个http Request:
GET /Protocols/rfc2616/rfc2616.html HTTP/1.1
Host: www.w3.org
User-Agent: Mozilla/5.0
(empty line)
如果是POST方法,在empty line后还包含请求体。
一个http Response:
HTTP/1.1 200 OK
Content-type: text/html
Content-length: 24204
(empty line)
and then 24,204 bytes of HTML code
go http包分为两种角色:http Client和http Server。http Client可以发送请求,比如写爬虫程序时语言扮演的角色就是http Client;http Server用来提供web服务,可以处理http请求并响应。

对于Request,作为http客户端(如编写爬虫类工具)常需要关注的是URL和User-Agent以及其它几个Header;作为http服务端(web服务端,处理请求)常需要关注的几项是:
URL
Header
Body
Form,、PostForm、MultipartForm
以下是完整的Request结构以及相关的函数、方法:混个眼熟就好了
type Request struct {
Method string
URL *url.URL
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error) // Server: x, Cleint: √
ContentLength int64
TransferEncoding []string
Close bool // Server: x, Cleint: √
Host string
Form url.Values
PostForm url.Values
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string // x
TLS *tls.ConnectionState
Cancel <-chan struct{} // x
Response *Response // x
}
func NewRequest(method, url string, body io.Reader) (*Request, error)
func ReadRequest(b *bufio.Reader) (*Request, error)
func (r *Request) AddCookie(c *Cookie)
func (r *Request) BasicAuth() (username, password string, ok bool)
func (r *Request) Context() context.Context
func (r *Request) Cookie(name string) (*Cookie, error)
func (r *Request) Cookies() []*Cookie
func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
func (r *Request) FormValue(key string) string
func (r *Request) MultipartReader() (*multipart.Reader, error)
func (r *Request) ParseForm() error
func (r *Request) ParseMultipartForm(maxMemory int64) error
func (r *Request) PostFormValue(key string) string
func (r *Request) ProtoAtLeast(major, minor int) bool
func (r *Request) Referer() string
func (r *Request) SetBasicAuth(username, password string)
func (r *Request) UserAgent() string
func (r *Request) WithContext(ctx context.Context) *Request
func (r *Request) Write(w io.Writer) error
func (r *Request) WriteProxy(w io.Writer) error
注意有哪些字段和方法,字段的详细说明见go doc http.Request。上面打了"x"的表示不需要了解的或者废弃的。
有一个特殊的字段Trailer,它是Header类型的,显然它存放的是一个个请求header,它表示请求发送完成之后再发送的额外的header。对于Server来说,读取了request.Body之后才会读取Trailer。很少有浏览器支持HTTP Trailer功能。
以下是完整的Response结构以及相关的函数、方法:混个眼熟就好了
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Uncompressed bool
Trailer Header
Request *Request
TLS *tls.ConnectionState
}
func Get(url string) (resp *Response, err error)
func Head(url string) (resp *Response, err error)
func Post(url string, contentType string, body io.Reader) (resp *Response, err error)
func PostForm(url string, data url.Values) (resp *Response, err error)
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)
func (r *Response) Cookies() []*Cookie
func (r *Response) Location() (*url.URL, error)
func (r *Response) ProtoAtLeast(major, minor int) bool
func (r *Response) Write(w io.Writer) error
其实有些直接从字面意思看就知道了。
URL
Http Header
读取标准输入
fmt包中提供了3类读取输入的函数:
- Scan家族:从标准输入os.Stdin中读取数据,包括Scan()、Scanf()、Scanln()
- SScan家族:从字符串中读取数据,包括Sscan()、Sscanf()、Sscanln()
- Fscan家族:从io.Reader中读取数据,包括Fscan()、Fscanf()、Fscanln()
其中:
- Scanln、Sscanln、Fscanln在遇到换行符的时候
停止 - Scan、Sscan、Fscan将换行符当作
空格处理 - Scanf、Sscanf、Fscanf根据给定的format格式读取,就像Printf一样
| Scan、Scanf和Scanln | Sscan、Sscanf和Scanln | bufio包 | |
|---|---|---|---|
从标准输入读取数据 |
从给定字符串中读取数据 |
读取标准输入 | |
| can | 换行输入 | ||
| canf | 指定分隔符“:” | ||
| canln | 以空格为间隔符输入 |
这3家族的函数都返回读取的记录数量,并会设置报错信息,例如读取的记录数量不足、超出或者类型转换失败等。
以下是它们的定义方式:
$ go doc fmt | grep -Ei "func [FS]*Scan"
func Fscan(r io.Reader, a ...interface{}) (n int, err error)
func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
func Fscanln(r io.Reader, a ...interface{}) (n int, err error)
func Scan(a ...interface{}) (n int, err error)
func Scanf(format string, a ...interface{}) (n int, err error)
func Scanln(a ...interface{}) (n int, err error)
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
因为还没介绍io.Reader,所以Fscan家族的函数暂且略过,但用法和另外两家族的scan类函数是一样的。
Scan、Scanf和Scanln
Scan家族函数从标准输入读取数据时,将以空格为分隔符分隔标准输入中的内容,并将分隔后的各个记录保存到给定的变量中。其中Scanf()可以指定分隔符。
例如,使用Scanln函数等待用户输入数据,或从管道中读取数据。下面的代码将等待用户输入,且将读取的内容分别保存到name变量和age变量中:
package main
import (
"fmt"
)
func main() {
var (
name string
age int
)
fmt.Print("输入姓名和年龄,使用空格分隔:")
fmt.Scanln(&name, &age)
fmt.Printf("name: %s\nage: %d\n", name, age)
}
因为Scanln()遇到换行符或EOF的时候终止读取,所以在输入的时候只要按下回车键就会结束读取。
运行它,将提示输入姓名:
请输入姓名和年龄,空格分隔:
周伯通 69
name: 周伯通
age: 69
同理Scanf()也在遇到换行符或EOF的时候终止读取行为。使用Scanf()的时候,需要给定格式化字符串形式:
例如:
fmt.Scanf("%s %d",&name,&age)
输入时,第一个字段会转换成字符串格式保存到name变量中,第二个记录会转换成整数保存到age中,如果转换失败,将不会进行保存。例如输入malongshuai aaa,由于aaa无法转换成int,所以age变量的值仍然为初始化的数值0。
Scanf可指定分隔符,其中上面的是%s %d中间的空格就是分隔符。例如下面指定:作为分隔符:
fmt.Scanf("%s : %d",&name,&age)
在输入时,必须按照以下格式进行输入:首先至少一个空格,然后一个冒号,再至少一个空格:
周伯通 : 23 // 或者连续多个空格 "周伯通 : 23"
name: 周伯通
age: 23
如果使用的是fmt.Scan(),则输入数据时可以换行输入,Scan()会将换行符作为空格进行处理,直到读取到了2个记录之后自动终止读取操作:
fmt.Scan(&name, &age)
请输入姓名和年龄,空格分隔:周伯通
87
name: 周伯通
age: 87
一般来说,只使用Scanf类函数比较好。
返回值
这些函数都有返回值:读取的记录数量和err信息。
以Scanln()为例:
func main() {
var (
name string
age int
)
fmt.Print("输入姓名和年龄,使用空格分隔:")
n, err := fmt.Scanln(&name, &age)
fmt.Printf("name: %s\nage: %d\n", name, age)
fmt.Println("records count:",n)
fmt.Println("err or not:",err)
}
输入:
malongshuai 23 // n = 2, err = nil
malongshuai // n = 1, err != nil
malongshuai long // n = 2, err != nil
malongshuai 23 23 // n = 2, err != nil
Sscan、Sscanf和Scanln
Sscan家族的函数用于从给定字符串中读取数据,用法和Scan家族类似。
func Sscan(str string, a ...interface{}) (n int, err error)
func Sscanln(str string, a ...interface{}) (n int, err error)
func Sscanf(str string, format string, a ...interface{}) (n int, err error)
例如:
package main
import (
"fmt"
)
func main() {
var (
name string
age int
)
input := "malongshuai 23"
fmt.Sscan(input, &name, &age)
fmt.Printf("name: %s\nage: %d\n", name, age)
}
使用Sscanf()可以指定分隔符:
input := "malongshuai : 23"
fmt.Sscanf(input, "%s : %d", &name, &age)
使用bufio中读取标准输入
除了fmt包的Scan类函数,bufio包也可以读取标准输入。当然,读取标准输入只是它的一个功能示例,它的作用是操作缓冲IO。
package main
import (
"bufio"
"fmt"
"os"
)
var inputReader *bufio.Reader
var input string
var err error
func main() {
inputReader = bufio.NewReader(os.Stdin)
fmt.Println("输入姓名:")
input, err = inputReader.ReadString('\n')
if err == nil {
fmt.Printf("The input was: %s\n", input)
}
}
其中NewReader()创建一个bufio.Reader实例,表示创建一个从给定文件中读取数据的读取器对象。然后调用读取器对象(Reader实例)的ReadString()方法,这个方法以\n作为分隔符,它的分隔符必须只能是单字符,且必须使用单引号包围,因为它会作为byte读取。ReadString()读取来自os.Stdin的内容后将其保存到input变量中,同时返回是否出错的信息。ReadString()只有一种情况会返回err:没有遇到分隔符。
ReadString会将读取的内容包括分隔符都一起放进缓冲中,如果读取文件时读到了结尾,则会将整个文件内容放进缓冲,并将文件终止标识符io.EOF放进设置为err。
通常无需单独定义这些变量,下面是更常见的用法。
inputReader := bufio.NewReader(os.Stdin)
input, err := inputReader.ReadString('\n')
关于包和go文件
每个go代码文件只能且必须使用package语句声明一个包,也就是说一个文件中不能包含多个包。
Go中有两种类型的包,或者说有两种类型的文件:
- 编译后成为可执行文件的包,也就是main包编译后的得到的文件
- 编译后成为共享库的包,只要go程序文件中声明的不是main包,就是库文件
注意:
在go的官方文档中将go的二进制可执行程序称为命令,有时候还会将go的源代码文件称为命令的源文件。可执行程序和包相反,包一般是作为"库"文件存在,用于导入而非用于执行
共享库中包含一些函数,这些函数比较通用,所以放进共享库方便函数复用。例如fmt包中的Println函数,到处都在使用这个函数,且因为fmt包是标准库(Standary library),无论是谁都可以去使用这个包。
有两种类型的库文件:标准库和第三方的库。标准库是随Go安装的时候放在go安装目录下的($GOROOT/src/),第三方库是放在workspace下的。关于workspace后文再说。
共享库可以被import导入(例如fmt包)。由于导入操作是在编译期间实现的,共享库中不应该包含任何输出型语句。
Go中对库文件要求比较严格,或者说强制性的规范。它要求库文件中package声明的包名必须和目录名称相同,且同一个目录下只允许有一个包,但同一个目录下可以有多个库文件片段,只不过这些库文件中必须都使用package声明它的包名为目录名。例如:
src/mycode
|- first.go
|- second.go
|- third.go
如果这三个文件都是库文件,则它们都必须且只能使用package mycode声明自己的包为mycode。go build的时候,会将它们合并起来。如果声明的包名不是mycode,go build会直接忽略它。
当然,对main包就无所谓了,它不是库文件,可以放在任何地方,对目录名没有要求。但如果使用go install,则有额外的要求,见后文。
库文件中的大小写命名
Go通过名称首字母的大小写决定属性是否允许导出:
- 首字母大写的属性是允许导出的属性
- 首字母小写的属性不允许被导出
所以当库文件被导入时,只有这个库文件中以大写字母开头的常量、变量、函数等才会被导出,才可以在其他文件中使用。
例如,库文件abc.go中:
func first() {}
func Second() {}
当导入这个包的时候,由于first()函数首字母小写,外界无法使用它,它只能在自己的包abc.go中使用,对外界不可见。大写字母开头的Second()函数会被导入,所以可用。
工作空间(workspace)
速览
- 通过环境变量
GOPATH设置workspace的路径 - Go编程人员一般将它们的Go代码放在一个
workspace下,当然,这不是必须的 - workspace包含一个或多个版本控制系统的仓库(如git)
- 每个仓库包含一个或多个package
- 每个package由单个目录下的一个或多个Go源文件组成,它们都必须声明目录名作为它们的包名
- package的目录路径决定导入包时import的路径
Go和其它编程语言在组织项目的时候有所不同,其它语言一般每个项目都有一个单独的workspace,且workspace一般和版本控制仓库进行绑定。
现在设置GOPATH环境变量,假设设置为/gocode
echo 'export GOPATH=/gocode' >>/etc/profile.d/gopath.sh
chmod +x /etc/profile.d/gopath.sh
source /etc/profile.d/gopath.sh
go env GOPATH确定是否正确:
$ go env GOPATH
/gocode
workspace目录结构
每个workspace都是一个目录,这个目录下至少包含三个目录:
- src:该目录用于存放Go源代码文件(也称为命令的源文件)
- bin:该目录用于存放可执行命令(即构建后可执行的二进制go程序,也称为命令文件)
- pkg:该目录用于存放共享库文件(即构建后非可执行程序的库包,也称为包对象文件)
括号中给的名称是go官方文档中常见的别名称呼。
所以,先创建这3个目录
mkdir -p /gocode/{src,pkg,bin}
GOPATH和GOROOT环境变量
GOPATH环境变量指定workspace的位置,用来指示go从哪里搜索go源文件/包,例如import时从哪个路径搜索包并导入。GOROOT环境变量用于指定go的安装位置。go需要导入包时,会从GOPATH和GOROOT所设置的位置处搜索包。
默认位置为$HOME/go(Unix)或%USERPROFILE\go%(Windows)。可以手动设置GOPATH环境变量的路径从而指定workspace的位置,可以指定为多个目录,多个目录时使用冒号分隔目录(Unix系统)或使用分号分隔目录(Windows系统)。注意,绝对不能将其设置为go的安装目录,即不能和GOROOT环境变量重复。
例如,windows下设置d:\gocode目录为GOPATH的路径:
setx GOPATH d:\gocode
Unix下设置$HOME/gocode目录为GOPATH的路径:
mkdir ~/gocode
export GOPATH=~/gocode
echo 'GOPATH=~/gocode' >>~/.bashrc
go env或go env GOPATH命令可以输出当前有效的GOPATH路径。
$ go env | grep GOPATH
GOPATH="/root/gocode"
$ go env GOPATH
/root/gocode
关于GO文档
关于fmt的Println函数详细用法,可去参考Go的官方文档:https://golang.org/pkg/fmt/#Println。当然,现阶段去看官方手册,那是在找死。
还可以使用go doc命令去查找各帮助文档。
例如,查看fmt包的帮助文档:
go doc fmt
查看fmt.Println函数的用法:
go doc fmt.Println
完整用法:
go doc