DNS Golang
2025-01-18
- https://github.com/mr-karan/doggo
- https://doggo.mrkaran.dev/docs/
安装
$ go install github.com/mr-karan/doggo/cmd/doggo@latest
$ doggo
NAME:
doggo 🐶 DNS Client for Humans
USAGE:
doggo [--] [query options] [arguments...]
VERSION:
unknown - unknown
EXAMPLES:
doggo mrkaran.dev Query a domain using defaults.
doggo mrkaran.dev CNAME Query for a CNAME record.
doggo mrkaran.dev MX @9.9.9.9 Uses a custom DNS resolver.
doggo -q mrkaran.dev -t MX -n 1.1.1.1 Using named arguments.
doggo mrkaran.dev --aa --ad Query with Authoritative Answer and Authenticated Data flags set.
doggo mrkaran.dev --cd --do Query with Checking Disabled and DNSSEC OK flags set.
doggo mrkaran.dev --gp-from Germany Query using Globalping API from a specific location.
FREE FORM ARGUMENTS:
Supply hostnames, query types, and classes without flags. Example:
doggo mrkaran.dev A @1.1.1.1
TRANSPORT OPTIONS:
Specify the protocol with a URL-type scheme.
UDP is used if no scheme is specified.
@udp:// eg: @1.1.1.1 initiates a UDP query to 1.1.1.1:53.
@tcp:// eg: @tcp://1.1.1.1 initiates a TCP query to 1.1.1.1:53.
@https:// eg: @https://cloudflare-dns.com/dns-query initiates a DOH query to Cloudflare via DoH.
@tls:// eg: @tls://1.1.1.1 initiates a DoT query to 1.1.1.1:853.
@sdns:// initiates a DNSCrypt or DoH query using a DNS stamp.
@quic:// initiates a DOQ query.
SUBCOMMANDS:
completions [bash|zsh|fish] Generate the shell completion script for the specified shell.
QUERY OPTIONS:
-q, --query=HOSTNAME Hostname to query the DNS records for (eg mrkaran.dev).
-t, --type=TYPE Type of the DNS Record (A, MX, NS etc).
-n, --nameserver=ADDR Address of a specific nameserver to send queries to (9.9.9.9, 8.8.8.8 etc).
-c, --class=CLASS Network class of the DNS record (IN, CH, HS etc).
-x, --reverse Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively.
--any Query all supported DNS record types (A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT, CAA).
RESOLVER OPTIONS:
--strategy=STRATEGY Specify strategy to query nameserver listed in etc/resolv.conf. (all, random, first).
--ndots=INT Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise.
--search Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list.
--timeout=DURATION Specify timeout for the resolver to return a response (e.g., 5s, 400ms, 1m).
-4, --ipv4 Use IPv4 only.
-6, --ipv6 Use IPv6 only.
--tls-hostname=HOSTNAME Provide a hostname for verification of the certificate if the provided DoT nameserver is an IP.
--skip-hostname-verification Skip TLS Hostname Verification in case of DOT Lookups.
QUERY FLAGS:
--aa Set Authoritative Answer flag.
--ad Set Authenticated Data flag.
--cd Set Checking Disabled flag.
--rd Set Recursion Desired flag (default: true).
--z Set Z flag (reserved for future use).
--do Set DNSSEC OK flag.
OUTPUT OPTIONS:
-J, --json Format the output as JSON.
--short Short output format. Shows only the response section.
--color Defaults to true. Set --color=false to disable colored output.
--debug Enable debug logging.
--time Shows how long the response took from the server.
GLOBALPING OPTIONS:
--gp-from=Germany Query using Globalping API from a specific location.
--gp-limit=INT Limit the number of probes to use from Globalping.
DNS 查询
$ doggo sendcloud.net a @223.5.5.5
NAME TYPE CLASS TTL ADDRESS NAMESERVER
sendcloud.net. A IN 60s 106.75.106.173 223.5.5.5:53
sendcloud.net. A IN 60s 106.75.106.166 223.5.5.5:53
$ doggo sendcloud.net a @223.5.5.5 --json
{
"responses": [
{
"answers": [
{
"name": "sendcloud.net.",
"type": "A",
"class": "IN",
"ttl": "60s",
"address": "106.75.106.173",
"status": "",
"rtt": "67ms",
"nameserver": "223.5.5.5:53"
},
{
"name": "sendcloud.net.",
"type": "A",
"class": "IN",
"ttl": "60s",
"address": "106.75.106.166",
"status": "",
"rtt": "67ms",
"nameserver": "223.5.5.5:53"
}
],
"authorities": null,
"questions": [
{
"name": "sendcloud.net.",
"type": "A",
"class": "IN"
}
]
}
]
}
$ doggo sendcloud.net a @223.5.5.5 --json | jq ".responses[].answers[].address"
"106.75.106.166"
"106.75.106.173"
查反解
$ doggo --reverse 101.44.172.1 @223.5.5.5
NAME TYPE CLASS TTL ADDRESS NAMESERVER
1.172.44.101.in-addr.arpa. PTR IN 300s hwsg1c1.email.engagelab.com. 223.5.5.5:53
$ doggo hwsg1c1.email.engagelab.com. a @223.5.5.5
NAME TYPE CLASS TTL ADDRESS NAMESERVER
hwsg1c1.email.engagelab.com. A IN 600s 101.44.172.1 223.5.5.5:53
Global Ping
$ doggo markjour.com --gp-from Germany,Japan --gp-limit 2
LOCATION NAME TYPE CLASS TTL ADDRESS NAMESERVER
Falkenstein, DE, EU, Hetzner
Online GmbH (AS24940)
markjour.com. A IN 600s 121.42.82.115 private
Osaka, JP, AS, Oracle
Corporation (AS31898)
markjour.com. A IN 600s 121.42.82.115 8.8.8.8
Golang
2024-09-09
基础示例
package main
import (
"fmt"
"os"
"strings"
"text/template"
"time"
)
const TEMPLTE = `{{ .Subject }}
Time: {{ .Time.Format "2006-01-02 15:04:05" }}
Source: {{ .Source }}
{{ .Body }}`
func main() {
tmpl01, _ := template.New("tmpl01").Parse("你好,{{ . }}")
tmpl01.Execute(os.Stdout, "世界")
fmt.Println()
fmt.Println(strings.Repeat("=", 80))
fmt.Println()
type Message struct {
Subject string
Time time.Time
Source string
Body string
}
// http://www.xinhuanet.com/politics/2020-02/08/c_1125546135.htm
subject := "国家监察委员会调查组已抵达武汉"
timeobj, _ := time.Parse("2006-01-02 15:04:05", "2020-02-08 13:49:38")
source := "新华社“新华视点”微博"
content := "中央纪委国家监委网站8日消息,国家监察委员会调查组已抵达武汉。经中央批准,国家监察委员会派出调查组赴湖北省武汉市,就群众反映的涉及李文亮医生的有关问题作全面调查。"
tmpl02, _ := template.New("tmpl02").Parse(TEMPLTE)
tmpl02.Execute(os.Stdout, Message{subject, timeobj, source, content})
}
语法说明
- 双花括号
{{ . }} // 输出当前变量
{{ .Name }} // 输出当前变量的 Name 字段
{{/* 注释 */}}
{{- /* 注释(去掉前后空格与换行) */ -}}
{{ if ... }} A {{ end }}
{{ if ... }} A {{ else }} B {{ end }}
{{ if ... }} A {{ else if ... }} B {{ else }} C {{ end }}
{{ range ... }} A {{ end }}
{{ range ... }} A {{ else }} B {{ end }}
{{ range . -}} {{ . }} {{ end -}}
{{ range $key, $val := . -}} ... {{ end -}}
Golang fasthttp
2024-01-28
Golang
2024-01-12
后期优化:
- items 按照链表的方式组织起来,按过期时间排序,加快清理速度。
或者另外设计一个数据结构,存储缓存过期时间。
放弃每次 Get 的时候判断是否过期。
package main
import (
"fmt"
"sync"
"time"
)
type CacheItem struct {
key string
value interface{}
expiration time.Time
}
type Cache struct {
items map[string]CacheItem // 缓存项
mu sync.Mutex // 锁保护并发读写
cleanupInterval time.Duration // 清理缓存的时间间隔
}
// 创建一个新的缓存实例
func NewCache(cleanupInterval time.Duration) *Cache {
c := &Cache{
items: make(map[string]CacheItem),
cleanupInterval: cleanupInterval,
}
// 启动后台清理任务
go c.startCleanup()
return c
}
// 启动后台定期清理过期缓存的任务
func (c *Cache) startCleanup() {
ticker := time.NewTicker(c.cleanupInterval)
for {
<-ticker.C
c.cleanUpExpiredItems()
}
}
// 清理过期缓存项
func (c *Cache) cleanUpExpiredItems() {
c.mu.Lock()
defer c.mu.Unlock()
// 清理有过期时间的缓存项
for key, item := range c.items {
if time.Now().After(item.expiration) {
delete(c.items, key)
}
}
// 打印清理日志(可以根据需要调整)
fmt.Println("Expired cache items cleaned up")
}
// 添加缓存项,支持过期时间
func (c *Cache) Set(key string, value interface{}, expiration time.Time) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = CacheItem{key: key, value: value, expiration: expiration}
}
// 获取缓存项
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
// 再检查有过期时间的缓存项
if item, found := c.items[key]; found {
// if item.expiration.IsZero() || time.Now().Before(item.expiration) {
// return item.value, true
// }
// // 如果缓存项已过期,则删除并返回未找到
// delete(c.items, key)
return item.value, true
}
return nil, false
}
// 删除缓存项
func (c *Cache) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.items, key)
}
func main() {
// 创建一个缓存实例,每 1 秒清理一次过期缓存
cache := NewCache(1 * time.Second)
// 设置缓存项(带过期时间)
cache.Set("key1", "value1", time.Now().Add(5*time.Second))
// 设置没有过期时间的缓存项
cache.Set("key2", "value2", time.Time{}) // 空时间表示永不过期
// 获取缓存项
if value, found := cache.Get("key1"); found {
fmt.Println("Found key1:", value)
} else {
fmt.Println("key1 not found")
}
if value, found := cache.Get("key2"); found {
fmt.Println("Found key2:", value)
} else {
fmt.Println("key2 not found")
}
// 等待 6 秒后,key1 会过期
time.Sleep(6 * time.Second)
// 再次获取缓存项
if value, found := cache.Get("key1"); found {
fmt.Println("Found key1:", value)
} else {
fmt.Println("key1 not found (after expiration)")
}
if value, found := cache.Get("key2"); found {
fmt.Println("Found key2:", value)
} else {
fmt.Println("key2 not found")
}
// 让清理任务继续运行
select {}
}
Clang Golang
2023-12-01
看到一片技术文章《改了一行代码,数组遍历耗时从 10.3 秒降到了 0.5 秒!》,试验了一下。
Golang 配置文件
2023-11-26
Golang Configuration tool that support YAML, JSON, TOML, Shell Environment (Supports Go 1.10+)
Golang
2023-07-05
仔细观察下面的例子就能知道问题在哪里了:
例子 1
package main
import "fmt"
func main() {
items := []int{1, 2, 3}
{
var all []*int
for _, item := range items {
all = append(all, &item)
}
fmt.Printf("%+v\n", all)
// [0xc00008c018 0xc00008c018 0xc00008c018]
// 输出的都是最后一个值!!!
for _, i := range all {
fmt.Printf("%+v, %+v\n", i, *i)
}
}
// fix it:
{
var all []*int
for _, item := range items {
item := item // 重点
all = append(all, &item)
}
for _, i := range all {
fmt.Printf("%+v, %+v\n", i, *i)
}
}
}
例子 2
package main
import "fmt"
func main() {
{
var prints []func()
for _, v := range []int{1, 2, 3} {
prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
print()
}
}
// 输出的是 3 3 3,而不是 1,2,3,Why?
// fix it:
{
var prints []func()
for _, v := range []int{1, 2, 3} {
v := v // 重点
prints = append(prints, func() { fmt.Println(v) })
}
for _, print := range prints {
print()
}
}
}
例子 3
package main
import (
"fmt"
"sync"
)
func main() {
items := []int{1, 2, 3}
{
wg := sync.WaitGroup{}
for _, v := range items {
wg.Add(1)
go func() {
// 会提示:loop variable v captured by func literal
fmt.Println(v)
wg.Done()
}()
}
wg.Wait()
}
// fix it:
{
wg := sync.WaitGroup{}
for _, v := range items {
wg.Add(1)
v := v // 重点
go func() {
fmt.Println(v)
wg.Done()
}()
}
wg.Wait()
}
}
这个例子可以改写成:
package main
import (
"fmt"
)
func main() {
items := []int{1, 2, 3}
done := make(chan bool)
{
for _, v := range items {
go func() {
// 会提示:loop variable v captured by func literal
fmt.Println(v)
done <- true
}()
}
for _ = range items {
<-done
}
}
// fix it:
{
for _, v := range items {
v := v // 重点
go func() {
fmt.Println(v)
done <- true
}()
}
for _ = range items {
<-done
}
}
}
我的理解
根据 Go 的设计思想,花括号内是一个独立的作用域,for 循环每一次也应该是独立的。
当前这次循环中的变量和上一次循环的变量应该是不一样的。
但实际上,根据运行结果来看,他们的地址是一样的。
闭包函数应该也是这样的,去原来的位置读相关变量,但是之前的位置写入了新的值。
这个设计是一个大坑,对于新人非常不友好。
从 1.21 开始支持根据环境变量 GOEXPERIMENT=loopvar
来启用新的订正版语义。
从 1.22 开始正式修改 for 语义。
Russ Cox 认为当前语义的代价很大,出错的频率高于正确的频率。
但是这次变更 for 语句语义的决定和之前的承诺有冲突(保证兼容性的基础就是语义不会被重新定义)。
但是好在 Go 已经为这种情况做好了足够的准备,即,go 支持在同一批编译中,按照不同包的 mod 文件中声明的 go 版本来对体现不同的语法特性。
就比如 A 包需要 1.22,然后依赖 B 包,需要 1.18,那么 A 包中的代码按照新的定义来,B 包中的代码按照旧的定义来。
https://github.com/golang/go/discussions/56010
Golang
2023-05-30
gotip 是官方推出的,从最新的开发分支下拉代码,编译生成 go 运行时的工具。
如果希望体验最新特性,可以在编译成功之后,可以直接用 gotip 取代 go 命令用来执行 go 程序。
gotip 就可以理解为开发版的 go。
go install golang.org/dl/gotip@latest
gotip
gotip: not downloaded. Run 'gotip download' to install to C:\Users\nosch\sdk\gotip
gotip download
Cloning into 'C:\Users\nosch\sdk\gotip'...
...
Building Go cmd/dist using C:\Program Files\Go. (go1.20.5 windows/amd64)
Building Go toolchain1 using C:\Program Files\Go.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for windows/amd64.
成功的时候:
---
Installed Go for windows/amd64 in C:\Users\nosch\sdk\gotip
Installed commands in C:\Users\nosch\sdk\gotip
Success. You may now run 'gotip'!
失败的时候:
# runtime/cgo
gcc_libinit_windows.c: In function '_cgo_beginthread':
gcc_libinit_windows.c:143:27: error: implicit declaration of function '_beginthread'; did you mean '_cgo_beginthread'? [-Werror=implicit-function-declaration]
143 | thandle = _beginthread(func, 0, arg);
| ^~~~~~~~~~~~
| _cgo_beginthread
cc1: all warnings being treated as errors
go tool dist: FAILED: C:\Users\nosch\sdk\gotip\pkg\tool\windows_amd64\go_bootstrap install std: exit status 1
Success. You may now run 'gotip'!
Golang
2023-05-29
Go 并没有支持集合类型,我们需要自己实现:
https://go.dev/play/p/uVDCiN4Cbpt
package main
import "fmt"
type Set map[string]bool
func (s Set) Add(item string) {
s[item] = true
}
func (s Set) Remove(item string) {
delete(s, item)
}
func (s Set) Contains(item string) bool {
_, exists := s[item]
return exists
}
func main() {
mySet := make(Set)
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
for item := range mySet {
fmt.Println(item)
}
fmt.Println(mySet.Contains("apple")) // 输出: true
fmt.Println(mySet.Contains("grape")) // 输出: false
mySet.Remove("banana")
fmt.Println(mySet.Contains("banana")) // 输出: false
}
注意:
- 使用 map 做底层存储,因此实现的 set 也是无序的
- map 不是线程安全的,如果有并发操作,需要加锁
- 如果真的要使用集合类型,应该再扩充一下交集,差集等方法
改进
参考 https://github.com/deckarep/golang-set
的设计:
https://go.dev/play/p/BKWT84lXfuz
package main
import "fmt"
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}
func (s Set[T]) Remove(item T) {
delete(s, item)
}
func (s Set[T]) Contains(item T) bool {
_, exists := s[item]
return exists
}
func main() {
mySet := make(Set[string])
mySet.Add("apple")
mySet.Add("banana")
mySet.Add("orange")
for item := range mySet {
fmt.Println(item)
}
fmt.Println(mySet.Contains("apple")) // 输出: true
fmt.Println(mySet.Contains("grape")) // 输出: false
mySet.Remove("banana")
fmt.Println(mySet.Contains("banana")) // 输出: false
}
优化点:
- 空结构体不占空间
- 泛型让代码复用性更好
Golang
2023-04-27
看到微信公众号 Go语言教程 的文章《golang 实现简单网关》才知道还有 httputil.ReverseProxy 这么个东西。
PS:这玩意儿有什么必要放在标准库?
还挺有趣,在作者示例的基础之上完善了一下,实现多服务,多后端节点的一个负载均衡(还可以再补上权重)。
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"time"
)
func main() {
addr := "127.0.0.1:2002"
backends := map[string][]string{
"service1": {"http://127.0.0.1:2003", "http://127.0.0.1:2004"},
}
reversePorxy := NewReverseProxy(backends)
log.Println("Starting httpserver at " + addr)
log.Fatal(http.ListenAndServe(addr, reversePorxy))
}
func reqConvert(req *http.Request, target *url.URL) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
if target.RawQuery == "" || req.URL.RawQuery == "" {
req.URL.RawQuery = target.RawQuery + req.URL.RawQuery
} else {
req.URL.RawQuery = target.RawQuery + "&" + req.URL.RawQuery
}
if _, ok := req.Header["User-Agent"]; !ok {
req.Header.Set("User-Agent", "")
}
req.Header.Set("X-Real-Ip", strings.Split(req.RemoteAddr, ":")[0])
}
func NewReverseProxy(backends map[string][]string) *httputil.ReverseProxy {
var targets = make(map[string][]*url.URL)
for srv, nodes := range backends {
for _, nodeUrl := range nodes {
target, _ := url.Parse(nodeUrl)
targets[srv] = append(targets[srv], target)
}
}
director := func(req *http.Request) {
segments := strings.SplitN(req.URL.Path, "/", 3)
if len(segments) != 3 {
return
}
srv := segments[1]
req.URL.Path = segments[2]
if _, ok := targets[srv]; !ok {
log.Printf("unknown path: %s", req.URL.Path)
return
}
rand.Seed(time.Now().UnixNano())
randomIndex := rand.Intn(len(targets[srv]))
target := targets[srv][randomIndex]
reqConvert(req, target)
}
modifyFunc := func(res *http.Response) error {
if res.StatusCode != http.StatusOK {
oldPayLoad, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
newPayLoad := []byte("hello " + string(oldPayLoad))
res.Body = ioutil.NopCloser(bytes.NewBuffer(newPayLoad))
res.ContentLength = int64(len(newPayLoad))
res.Header.Set("Content-Length", fmt.Sprint(len(newPayLoad)))
}
return nil
}
return &httputil.ReverseProxy{Director: director, ModifyResponse: modifyFunc}
}
func singleJoiningSlash(a, b string) string {
aslash := strings.HasSuffix(a, "/")
bslash := strings.HasPrefix(b, "/")
switch {
case aslash && bslash:
return a + b[1:]
case !aslash && !bslash:
return a + "/" + b
}
return a + b
}