gin.Context 异步调用踩坑
Tag gin, context, 异步, goroutine, on by view 41

事情的背景是这样了,我在gin框架的action中有个异步调用逻辑,然后异步调用需要使用context.Context接口作为参数传入,异步调用的模块中会从context中取request_id作为追踪追踪标记。于是我就直接讲*gin.Context作为参数传入了异步调用中。

然后灾难就发生了,我日志记录中能查到当前请求的request_id,但是发现条数不对,异步请求中记录的日志条数有10条,我用request_id去搜索,只查到了4条,最后我只能添加另外标记,通过另外的标记查询到10条日志,让我奇怪的是,另外6条日志的request_id却不是当前请求的request_id。

最后,我发现gin.Context专门有个Copy方法,是将context拷贝一份,我调用Copy拷贝一份context之后将拷贝的context传入异步调用,果然10条日志的request_id一致了。看来,从gin框架传入进来的context在不同的action中是复用的。然后在请求处理完毕之后,会给其他请求复用,这样传给异步调用模块的context中的request_id已经变了。

r.GET("/async", func(c *gin.Context) {
	// 需要搞一个副本
	copyContext := c.Copy()
	// 异步处理
	go func() {
		time.Sleep(3 * time.Second)
		log.Println("异步执行:" + copyContext.Request.URL.Path)
	}()
})

这里必须要谨记,如果需要在action中异步调用并使用context传参,必须要将context.Copy()之后再传入。否则context就会被其他协程修改。


golang程序并发读写全局变量,导致空指针异常
Tag golang, 数据竞争, 线程安全, on by view 115

最近将一个golang程序由原本的单线程改为双线程处理日志解析,在生产环境节点上运行发现出现了一个空指针异常,异常信息如下:

[E] 2023/02/27 11:53:25 panic.go:838: parse error:runtime error: invalid memory address or nil pointer dereference,line:Feb 27 11:53:25 xxxxx.site nginx: [xxxxxxxxxxxxxxxxxxx] [27/Feb/2023:11:53:25 +0800] [https] [120.232.31.196:443] [xxxxx.map.xx.com] [39.144.41.41:37647] [200] [30.171.153.132:10000] [200] [3339597] [POST /tr?mllc HTTP/1.1] [621] [152] [xxxxxxx.map.xx.com] [Dalvik/2.1.0 (Linux; U; Android 10; HMA-AL00 Build/HUAWEIHMA-AL00)] [-] [0.008] [0.008] [0.004] [0.008] [46795] [311532943564] [1][1:46:0:0:0:0:0] [ECDHE-RSA-AES128-GCM-SHA256] [TLSv1.2] [r][-] [-] [n] [2358837] [-1] [1677470005.398|5|51|126|-1|-1|126|126|130|-1|-1|130|134|134|134|134#200|200|8|152|120.232.31.196|30.171.153.132:10000|0|0] [POST] [/tr] [mllc] [HTTP/1.1] [39.144.41.41] [-] [-] [-] [-] [-] [-] [-] [-] [-] [-] [169.254.213.29:50937] [0]

修改程序,打印出异常栈 x7s4uzea

发现异常发生在代码133行,代码如下 0zhhoxbp

很明显这里不太可能出现空指针,除非运行到这一行的时候sl对象被置为nil,但是我很确定这里不存在其他线程共享sl的情况,也就是不可能被其他线程置为nil,何况,我这里没有任何操作将sl置为nil,百思不得其解。最后发现key1key2这两个变量是全局变量,全局变量在多线程环境下会存在数据竞争问题。原本定义如下

var (
	uriSep *regexp.Regexp = nil

	key1 = ""
	key2 = ""
)

可以看到key1,key2被定义为全局变量,忘记修改了。修改之后,神奇的发现,空指针异常已经不再存在了。

我在发现这个问题之前,在本地开发服务器上尝试重现,但是一直未能重现出来,估计是我本地qps不够高,所以难以复现,生产环境qps是3w到4w左右,这个空指针异常呈现无规律的隔几秒钟出现一次。

golang中,双协程(绑定在双M和双cpu上)中同时读写一个共享变量导致空指针异常,这种情况我还是第一次遇见,以前遇到这种双协程读写一个共享变量的情况都是数据错乱,并没有空指针。据说这种线程安全、数据竞争导致的空指针异常在C++中也是常见的情况。所以,我在想,这里会不会是因为我将2个协程绑核了,所以在双线程绑双核的情况下更容易复现呢。


为go程序的协程绑核
Tag golang, 绑核, on by view 723

最近在公司的日志处理程序上做性能优化,用到了绑核的情况。背景是这样的,nginx进行http转发,产生日志,然后我们的程序读取日志,用lexer分词器对日志分隔字段,并且对字段进行统计聚合上报,生成监控。日志处理程序最开始是在单个goroutine里进行读取并且解析操作了,但是在核数比较多的大机器上,发现日志生成太快,解析程序处理不过来,在日志rotate的过程中会发生丢失日志的情况。于是针对这个情况进行了优化。

用pprof发现,性能消耗最大的部分是lexer,lexer其实就是个分词器,编译器中常用的技术,逐字符读取每行日志,然后基于状态机状态标记对日志的字段进行分割,中间涉及到的状态也不算太多,主要是双引号(“”)作为定界符提取字符串字段,方括号([])作为定界符提取字符串字段,空字符(空格、\t)和竖线符(|)作为分隔符分隔字段,转义符()对字符串中的字符串定界符(”[])进行转义,总体来说,状态不算复杂,其中也针对lexer优化过尽量减少变量分配和杜绝变量逃逸,lexer实在是已经无可优化了。

于是只好从其他方面下手,首先就是cpu切换的性能损耗。众所周知golang中没有线程的,golang中只有协程(goroutine),而防止cpu切换的性能损耗只有绑核这个方法,具体就是讲指定的核绑定到某个线程上,这样这个线程就会只在这个指定的核上运行,不会被系统切换到其他核上,这样也就不会产生切换的损耗了。但是golang程序中只有goroutine,不能直接操作线程。其实我们是有办法对goroutine进行绑核的。

首先,使用go里面的runtime.LockOSThread()将当前goroutine绑定到它所在的M线程,这样,这个goroutine就不会在M线程之间切换了;然后,我们可以使用cgo,调用pthread_self获取当前协程所在M线程的线程ID,并调用CPU_SET对这个线程ID设置cpuid绑定。具体如下

package affinity

/*
#define _GNU_SOURCE
#include <sched.h>
#include <pthread.h>

int lock_thread(int cpuid) {
  pthread_t tid;
  cpu_set_t cpuset;

  tid = pthread_self();
  CPU_ZERO(&cpuset);
  CPU_SET(cpuid, &cpuset);
  return  pthread_setaffinity_np(tid, sizeof(cpu_set_t), &cpuset);
}

pthread_t current_thread_id() {
  pthread_t tid;

  tid = pthread_self();

  return tid;
}
*/
import "C"

import (
	"fmt"
	"runtime"
)

// SetAffinity 设置CPU绑定
func SetAffinity(cpuID int) (uint64, error) {
	runtime.LockOSThread()
	ret := C.lock_thread(C.int(cpuID))
	tid := uint64(C.ulong(C.current_thread_id()))
	if ret > 0 {
		return 0, fmt.Errorf("set cpu core affinity failed with return code %d", ret)
	}
	return tid, nil
}

这样一来,我们只需要在goroutine中调用SetAffinity就可以将指定的cpuid和当前goroutine进行绑定。这样就实现了goroutine的绑定。

我将日志处理程序改为在主协程中读取文件并且通过channel分发日志行,然后在2个goroutine执行最占cpu的lexer及后续处理,并且在这两个goroutine中绑定cpuid为1,2。

qdx9l7z5

图中可以看到,两个处理日志的goroutine绑定了1,2两个cpu,并且不会切为其他cpu,这两个cpu都在处理日志,所以cpu占用都比较高,相当于把原来一个核处理的任务分担到2个核上了。


golang客户端取消http请求
Tag golang, cancelable, http, on by view 3459

首先,创建带有Cancel Context的http请求

var cx context.Context
cx, req.cancel = context.WithCancel(context.Background())

if req.httpreq, err = http.NewRequest(req.method, req.url, reqbody); err != nil {
    return
}
req.httpreq = req.httpreq.WithContext(cx)
req.httpreq.Header = req.Headers
req.httpreq.ContentLength = reqbodyLength

然后,需要停止请求的时候调用req.cancel()方法

if req.cancel != nil {
    req.cancel()
    fmt.Println("running cancel...")
}

return nil

这样才能够客户端强行主动取消正在进行的http连接。就是这么简单


关于阿里云ESC上go语言项目编译6l: running gcc failed: Cannot allocate memory
Tag golang, 编译, 内存不够, swap, on by view 4310

前段时间将自己的阿里云服务器上的系统由centos 6.5换为了ubuntu 14,其他的硬件配置都没有发生改变,将服务器上的数据恢复并且重新安装了golang的编译环境后,发现使用go build编译稍微大一点的golang项目就会报错:

/usr/local/go/pkg/tool/linux_amd64/6l: running gcc failed: Cannot allocate memory

一直想不通为啥换了个系统就会报这个错,字面意思是gcc分配内存失败,应该是内存不够用,机器配置是1G内存,free -m 发现尚有400M的内存未使用,难道剩余400M的内存还不够go build命令编译代码使用?好吧,既然如此我就给它释放内存,kill掉众多的进程之后再进行go build编译,发现又可以编译了。之后发现偶尔能编译偶尔又不能编译,看样子确实是内存不够,可是为啥之前的centos系统上没有出现这种状况呢,一直不相信简单的“内存不够”就可以解释这一问题,因为之前的centos系统上是正常的,我甚至觉得可能是gcc版本的问题,猜测只有较高版本的gcc才会报这个错误。后来也曾在“golang天朝”论坛上发过帖子,并表达自己的猜测,认为不是内存不够这么简单,结果被别人鄙视不看英文……

不想花钱升级机器硬件,难道我只有装回centos?今天执行free -m偶然间注意到了swap的数值貌似一直是空的,我思考若是我添加swap交换空间是否能解决这一问题呢,毕竟swap其实就是用硬盘空间虚拟出的内存,一个内存的缓冲区。于是就给它加了个1G的文件作为swap,居然直接就可以用go build,再也不用担心gcc对我说Cannot allocate memory了。简单的记录一下添加文件作为swap的步骤:

  • 创建1个1GB的file

sudo dd if=/dev/zero of=/mnt/1GB.swap bs=1M count=1024
  • 格式化为Swap file

sudo mkswap /mnt/1GB.swap
  • 把swap file加入到系统中

sudo swapon /mnt/1GB.swap
  • 将swap永久添加
    在/ect/fstab中加入新的Swap分区

sudo gedit /etc/fstab
  • 在最后加入下列内容

/mnt/1GB.swap none swap sw 0 0

最后,free -m 命令可以看到swap的数据如下

             total       used       free     shared    buffers     cached
Mem:           992        903         88          0         57        188
-/+ buffers/cache:        656        335
Swap:         1023          0       1023

1G的内存交换区文件已经创建。


Goj开发过程中遇到gcc进程变为孤儿进程的问题
Tag Goj, 孤儿进程, deamon, Online Judge, on by view 3601

尽管现在已经放假,没有太多的心思放在开发上,但是Goj项目开发最近一直在缓慢的保持着。最近主要是处理judger与ojsite通信的问题,其过程中并没有遇到什么特殊的问题,TCP以及HTTP协议什么的都不成问题。但是在今天调试提交编译任务时却发现了一个有趣的事情,那就是gcc在特定情况下变成了deamon进程。

事情经过是这样的,但凡judger都必须经历#include "/dev/random"的考验,至於这是什么、为什么我也懒得解释,下面引用知乎上的一段话,感兴趣的自己看全文

小心编译期间的一些“高级功能”,比如 C 的 include 其实是有很多巧妙的用法,试试看在 Linux 下 #include "/dev/random" 或者 #include "/dev/tty" 之类的(这两个东西会把网络上不少二流 OJ 直接卡死……)。

而我的judger是这么解决这个问题的,创建一个线程监控编译子进程,超过一定时间kill。这样原本是没有太大的问题的,而且我也相信这也是一种正确的解决方案,但问题是我并不是直接用judger去调用gcc的,而是将gcc包装到了shell脚本,然后调用sh去执行脚本,这样做也是初期考虑到灵活性而决定的。于是judger的子进程是bash,而gcc是bash的子进程,编译任务阻塞后kill掉的是sh,于是gcc变为了孤儿进程,被init进程收养。

illeage_include.png

orphan.png

这倒不是什么难题,只要将进程改为能够kill掉gcc及其子进程便可解决,但是若是忽略这个问题将会是一个灾难,每一次#include "/dev/random"都将会让服务器产生deamon化的gcc进程,时间一久再多的内存都将会被gcc吃掉。



beego中使用fis
Tag beego, fis, on by view 9017

前段时间接触到了fis,后来发现它正是我所寻找的一类前端工具。之前在byvoid大神的博客上看到前端html源码中引用的js和css都是在文件名后面附加了随机串。如/css/style-882e60051fc11fd2558e888585fc3950.css。我感觉这个做法应该有他的深意。接触fis后才知道,文件md5映射之后可以在服务器端开启静态文件永久缓存,如此一来可以最大化利用浏览器的缓存机制,起到加快页面加载速度节省流量的效果。由于md5映射的文件名是唯一的,所以不用担心缓存导致的前端问题。这一技术在许多其他的网站上也有用到,例如百度、twitter等,其中fis就是出自百度,据fis文档介绍,其中某些创意是受twitter启发的。

最近一直在用beego做项目,于是打算将fis的md5映射机制迁移到beego上来,fis官网文档上介绍了fis在go web的实现demo,不过它是基于martini框架的,在多番查阅文档后,我觉得若是抛开模版内的资源文件自动打包这一功能单纯实现md5映射,我能够自己将其在beego上实现。

fis中带md5映射的前端资源全部记录于map.json文件中,fis项目编译后会自动生成map.json文件,示例如下

{
    "res": {
        "js/global.js": {
            "uri": "/static/js/global_f39ad61.js",
            "type": "js"
        },
        "js/user.js": {
            "uri": "/static/js/user_42dbc3b.js",
            "type": "js"
        },
        "octicons/octicons.scss": {
            "uri": "/static/octicons/octicons_1b828d3.css",
            "type": "css"
        },
        "sass/index.scss": {
            "uri": "/static/css/index_b855d1c.css",
            "type": "css"
        },
        "sass/login.scss": {
            "uri": "/static/css/login_c8abcc0.css",
            "type": "css"
        },
        "sass/register.scss": {
            "uri": "/static/css/register_b76a400.css",
            "type": "css"
        },
        "sass/style.scss": {
            "uri": "/static/css/style_462f7a1.css",
            "type": "css"
        }
    },
    "pkg": {}
}

这其中"res"项就是资源映射表,"js/global.js"以及"js/global.js"等是编译前的文件,即fis项目源文件,而"/static/js/global_f39ad61.js"以及"/static/octicons/octicons_1b828d3.css"等是编译后的文件,网站应当加载编译后的文件,而编译后的文件名是变动的,每次修改源文件并编译后,编译后的文件名md5值便会变化,不过编译前的文件名是不变的,因此,思路是通过编译前文件名得到对应的编译后的文件名,然后加载到网页。beego实现如下:

在工具包utils里有

package utils

import (
	"fmt"
	"github.com/astaxie/beego"
	"github.com/gogather/com"
	"html/template"
)

// fis map
func Fis(key string) template.HTML {
	var text string
	content := loadMap()
	json, _ := com.JsonDecode(content)
	json = json.(map[string]interface{})["res"]
	if fileMap, ok := json.(map[string]interface{}); !ok {
		fmt.Println("map.json id illeage!")
	} else {
		for tmpKey, views := range fileMap {
			uri, ok := views.(map[string]interface{})["uri"].(string)
			if !ok {
				fmt.Println("error in map.json")
			}

			fileType, ok := views.(map[string]interface{})["type"].(string)
			if !ok {
				fmt.Println("error in map.json")
			}

			if tmpKey == key {
				if fileType == "css" {
					text = `<link rel="stylesheet" href="` + uri + `">`
				} else if fileType == "js" {
					text = `<script src="` + uri + `"></script>`
				}
			}
		}
	}

	return template.HTML(text)
}

// load map.json
func loadMap() string {
	mapPath := beego.AppConfig.String("static_map")
	mapContent := com.ReadFile(mapPath)
	return mapContent
}

其中Fis函数便是由编译前的文件名查找编译后的文件名。接下来是注册模版函数

beego.AddFuncMap("asset", utils.Fis)

然后你就可以在模版中这样引入js或者css文件了

{{template "inc/header.tpl" .}}
{{asset "sass/login.scss"}}
	<div class="info">&nbsp</div>
	<div class="login">
		<form action="/login">
			<ul>
				<li><label for="">用户名</label><input type="text" name="username" id=""></li>
				<li><label for="">密码</label><input type="password" name="password" id=""></li>
				<li class="login-btns">
					<button class="btn">{{i18n "login"}}</button>
					<a class="btn oauth-login" href="https://github.com/login/oauth/authorize?client_id={{.github_client_id}}&scope=user,public_repo" target="_blank"><span class="octicon octicon-logo-github"></span>登录</a>
				</li>
			</ul>
		</form>
	</div>
{{asset "js/user.js"}}
{{template "inc/footer.tpl" .}}

其中

{{asset "sass/login.scss"}}

便是引入"sass/login.scss"对应的编译后的css文件。

如此,便实现了利用fis的md5映射功能,现在可以放心开启js和css的强制缓存了。最后附上我自己的项目地址供大家借鉴https://github.com/duguying/ojsite 。


写在网站变迁之后
Tag 网站, 备案, wordpress, go, 博客, on by view 5810

最近几天一直忙于这学期的课程设计作业,无法抽空写点什么。其实这次博客变动之后早就想写点东西记录一下本次博客搬迁的过程。

本次博客搬家是从原先的位于英国的Hostinger空间搬迁至国内的阿里云服务器,搬迁的原因主要有两个:第一个原因是国内访问国外的服务器线路极其的不稳定,经常被和谐或者是访问速度很慢;第二个原因是我已经决定并开始使用go语言重构了博客系统,弃用原来的基于php的wordpress博客系统,这一次用go语言重构博客也是一个挑战。

博客变迁记载

  • 2011-2012年左右,博客是基于php的wordpress系统,曾放置于“浦东信息港”那个免费空间提供商,相比于国内的免费空间这一家还算不错(不排除有我不知道的更好的),他提供香港机房版免备案空间,同时也提供单个MySQL数据库(不是整个MySQL服务器),可是有两个缺点:访问速度慢;不支持curl服务(这意味着不能在线安装wordpress的各种插件)。

  • 2013年,我申请了域名 duguying.net ,使用修改版的wordpress将网站放置于百度云,并且绑定了域名,可是不久之后百度云开始检查备案,未备案的网站一律停止域名解析,再到后来百度云升级了,可是升级后的系统却是非常的不方便使用,在到后来百度云开始大张旗鼓的收费了,感觉再也不爱百度云了。

  • 2014年上半年,将网站迁移至Hostinger,之前百度云上面的数据有部分已经丢失了,可是好景不长,Hostinger免费又是国外的免备案服务器,估计注册的人不少,那么各种言论也不少,相信触动了某的逆鳞的言论也是不少,因此就会偶尔遭遇和谐,对于这种各网站公用ip的空间来说,一个网站被和谐意味着相同ip的所有网站空间全部被和谐,空间提供商是老外,别人可不在意什么和谐。另外,Hostinger经常会出现CPU过载,然后你的网站就会自动跳转到一个警告页面,对于这个表示无语。最后,网速的确很差。

  • 2014年下半年,手痒了,想搞一个云服务器,把网站全部迁移至拥有独立造作系统、拥有root账户的云服务器或者是VPS。本人一开始是表示不喜欢备案的,因为我并不认为我的言论会导致什么坏的事情所以觉得自己没必要被审查,然后早就听说备案特别复杂,是一件很费神的事情。于是,我对比了国内外VPS/云服务器的价格之后,决定先在DigitalOcean购买一个月的VPS试用一下。买下之后却遇到了一系列的问题(将帐号锁定禁止开通主机),各方谷歌之后发现甚至有人大骂DigitalOcean野蛮终止自己的服务器导致大量的客户丢失(链接),硬着头皮看完英文,我想糟了该不会是被老外坑了吧,最后还是决定通过沟通的方式看能不能解决,于是我给客服发送用我那蹩脚的英文写的电子邮件,邮件中描述了我所遇到的状况并表示希望能够解除锁定,等待了半日的时间,终于收到了回复的电邮,帐号也解锁了。于是,立即创建了一个虚拟主机,ssh登陆之,最后发现ssh竟然卡出翔……我在putty上敲了一行的命令,没反应,大概过了十来秒屏幕突然一下子又出现了刚才敲的一行命令,按删除键没反应,再按又没反应,十来秒后一下子删除了N个字符,删多了。这样的用户体验,谁能忍!毕竟是国外的服务器……

  • 2014年7月左右,DigitalOcean那卡出翔的用户体验让我放弃了继续试用linode,听说linode可以选择日本的数据中心,网速还不错,据说大神byvoid的个人博客就是放在linode,可是又据说这个日本的数据中心容易被和谐。于是,恰逢遇到阿里云促销“0元半年体验”,申请成功后发现这个是不包括网络带宽的,没办法,一咬牙就花了127大洋买了6个月的带宽。ssh登录,速度很不错,速度上感觉与本地虚拟机相似比较爽,没办法了,买了国内的云服务器,乖乖备案吧。

  • 2014年8月,既然买了一个独立的云服务器那么就得用到实处,我决定使用go语言重构博客了,第一周看了《Go语言编程》那本书的大部分,第二周开始基于Beego框架构建博客系统,第三周博客后台完成大部分,开始构思前端界面并且开始准备备案的事情。终于要备案了,这是我第一次备案,备案使用的是阿里云系统,阿里云的客服处理事情的速度还是比较快的,提交初审后过了两小时就有客服妹纸打电话说明相关信息并且告诉我我起得网站名称不行,需要改一下更容易通过,于是就按照客服妹纸的建议改为“大俊的个人网站”(我提交的名称是“大俊哥之家”),并且客服告诉我我的域名持有人名称与申请备案人名称不一致,当时域名是在oray注册的,注册信息填写英文名的时候填写的是Rex Lee,因此显示为Rex Lee,客服建议我讲域名转到阿里云。后来我发现阿里云和万网居然合并了(万网被阿里云收购),记得我上次登录万网时候那时还没有合并,域名价格都是特贵的,所以当初才选择了相对便宜的Oray,可是现在一看,域名居然白菜价,转入39,注册49,续费55,我果断决定转入了,可是,有另外一件麻烦事情,当初Oray帐号注册时候没有实名认证,Oray规定只有实名认证的用户才能够转出域名,没办法,当初注册帐号填写的是企业账户,我现在是无法实名认证的,最终决定注册一个小号,将域名在帐号间转移(花了我10块大洋),然后将小号实名认证,Oray的客服效率真心查啊,帐号实名认证花了4天,这还是我在他官方bbs上催他们才有这效率的,终于,实名认证成功了,也成功获取了域名转移密码,申请转入万网成功,然后又等了将近5个工作日,终于成功转过来了,在原域名商那儿还有5个月的时间也一同转过来了,这是我没想到的。最后发现,国际域名.net根本就不支持中文持有人信息Orz...不过我还是决定将Rex Lee改为中文名的汉语拼音。域名转入的事情告一段落了,与此同时,我申请了阿里云备案的幕布,自己用他的幕布拍照,传照片以及各种资料,然后快递资料。接下来便是漫长的等待了,据说20个工作日可以得到备案结果,还好没用20天,备案号终于发下来了。早已经准备好的网站系统终于可以启动了,我霸气的在putty中敲下service nginx start启动了前端服务器。网站开通。

折腾了这么久,网站总算是通了,我想以后我会继续续费,将这个网站继续下去,go语言才刚刚开始,我也是刚刚开始学习go语言,后面的路还很长,我看好golang。也会坚持写博客。


Go语言新手应该知道的几点
Tag golang, 初学者, 包导入, 变量命名, on by view 5959

1. 变量与函数命名

在go语言中变量与函数的命名是有着特殊的限制的,比如,一个函数functionname与函数FunctionName是有着不同的特点的。函数名第一个字母大写表示此函数可以被其他文件中的代码所调用,而函数名第一个字母小写的函数却只能在当前文件中被调用。初学者往往会发现自己正确的导入了包,但是却无法调用包里面的函数,这时候请看一下自己定义的函数名的首字母的大小写情况。因此,建议Go语言变量命名一律使用骆驼命名法(FunctionName/functionName),而不是蛇形命名法(function_name)。

2. 包导入import

(1) 相对路径

import "./model" //当前文件同一目录的model目录,但是不建议这种方式来import

(2) 绝对路径

import "shorturl/model" //加载gopath/src/shorturl/model模块

(3) 点操作

import( . "fmt" ) 
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")

(4) 别名操作
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

import( f "fmt" ) //别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world")

(5) _操作

import ( "database/sql" 
         _ "github.com/ziutek/mymysql/godrv" 
        )
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数