├── .gitignore ├── LICENSE ├── README.md ├── github ├── api.go ├── api_test.go └── urlparse.go ├── go.mod ├── logging └── printer.go ├── main.go ├── parser └── markdown.go └── requests └── requests.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Jetbrains 17 | .idea/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Kevin Xiao 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sort-awesome-lists 2 | 3 | Sort awesome lists by the number of stars in each GitHub repository, for each sub-heading / section in the list. 4 | 5 | For example: 6 | - [**awesome-go** sorted by number of stars](https://gist.github.com/kvnxiao/cb432fca8cd9b59e325286b8f33cf53d) 7 | - [**awesome-rust** sorted by number of stars](https://gist.github.com/kvnxiao/fe8cd6ca03978a2ee69e36a37251bcd2) 8 | - [**awesome-kotlin** sorted by number of stars](https://gist.github.com/kvnxiao/5f809440525304c918b553b4bbc8cd73) 9 | - [**awesome-java** sorted by number of stars](https://gist.github.com/kvnxiao/dfea78544dd74953453ba74f6e59ee6f) 10 | 11 | This is a CLI application written in Go and uses 0 external dependencies. 12 | 13 | Sorting by stars implies parsing the original awesome-list `README.md` file and outputting a modified version where each section is sorted in descending order by the number of stars (for each valid github repository). 14 | 15 | GitHub repository detection involves checking each markdown bullet point if it contains a `username.github.io/repo` or `github.com/username/repo` link. Otherwise, if a project website is linked, the application will attempt to download and parse the webpage to check if a GitHub repository link exists within the HTML. 16 | 17 | ## How to use 18 | 19 | `sort-awesome-lists` is a CLI application. Build it and run in your terminal. 20 | 21 | ### Building 22 | 23 | ``` 24 | go build -o sort-awesome-lists main.go 25 | ``` 26 | 27 | Creates an executable file called `sort-awesome-lists` in your directory. Run in your terminal with `./sort-awesome-lists` 28 | 29 | ### Usage 30 | 31 | ``` 32 | Usage of sort-awesome-lists: 33 | -bs int 34 | number of concurrent requests to send to GitHub API at a time, per each block found. (default 5) 35 | -o string 36 | name of file to write output to if set, otherwise prints to stdout 37 | -t string 38 | GitHub personal access token 39 | -v prints debug messages to stdout if true (default = false) 40 | ``` 41 | 42 | A GitHub personal access token is **required** by the `-t` flag as this CLI application hits the GitHub API for repository statistics. The token allows one to access the GitHub API at a rate-limit of 5000 requests per hour. A personal access token with 0 permissions checked can be generated and used (go here to create one if you don't already have one: https://github.com/settings/tokens) 43 | 44 | This tool currently supports `username.github.io/repo` and `github.com/username/repo` detection. 45 | 46 | #### Example: 47 | 48 | ``` 49 | ./sort-awesome-lists -t="$token" -o="awesome-go-sorted.md" https://raw.githubusercontent.com/avelino/awesome-go/master/README.md 50 | ``` 51 | where `$token` is your github personal access token. 52 | 53 | The above example will download and parse the markdown file from `https://raw.githubusercontent.com/avelino/awesome-go/master/README.md`, and output a sorted markdown output in a file called `awesome-go-sorted.md` in the same working directory. 54 | 55 | ### Known Issues / Gotchas 56 | 57 | For entries in a list that do not directly link to `github.com/username/repo` or `username.github.io/repo`, the webpage will be downloaded and parsed to check if a GitHub repository link exists within the HTML. This means that this tool will be unable to pick up any links for websites that use a JavaScript framework and require JavaScript to render the page. 58 | -------------------------------------------------------------------------------- /github/api.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/kvnxiao/sort-awesome-lists/logging" 9 | "github.com/kvnxiao/sort-awesome-lists/requests" 10 | ) 11 | 12 | const ( 13 | HostName = "github.com" 14 | reposEndpoint = "https://api.github.com/repos" 15 | ) 16 | 17 | func GetReposEndpoint(repoPath string) string { 18 | return reposEndpoint + repoPath 19 | } 20 | 21 | type Repository struct { 22 | StargazersCount int `json:"stargazers_count"` 23 | Message string `json:"message,omitempty"` 24 | } 25 | 26 | func fetchRepoJson(repoURL string, token string) Repository { 27 | resp, err := requests.Get(repoURL, map[string][]string{ 28 | "Authorization": {"token " + token}, 29 | }) 30 | if err != nil { 31 | logging.Verbosef("an error occurred in fetching repository %s: %v", repoURL, err) 32 | return Repository{} 33 | } 34 | defer resp.Body.Close() 35 | decoder := json.NewDecoder(resp.Body) 36 | var repo Repository 37 | err = decoder.Decode(&repo) 38 | if err != nil { 39 | logging.Verbosef("could not decode JSON body for repository %s", repoURL) 40 | return Repository{} 41 | } 42 | return repo 43 | } 44 | 45 | func GetRepoStars(repoURL string, token string) int { 46 | return getRepoStars(repoURL, token, 5) 47 | } 48 | 49 | func getRepoStars(repoURL string, token string, retries int) int { 50 | repo := fetchRepoJson(repoURL, token) 51 | if repo.Message != "" && repo.Message != "Not Found" { 52 | if retries > 0 { 53 | logging.Verbosef("temporary error message for repo %s: %s. Retrying...", repoURL, repo.Message) 54 | time.Sleep(500 * time.Millisecond) 55 | return getRepoStars(repoURL, token, retries-1) 56 | } else { 57 | fmt.Printf("failed to retrieve stats for %s after 5 retries\n", repoURL) 58 | return 0 59 | } 60 | } 61 | return repo.StargazersCount 62 | } 63 | -------------------------------------------------------------------------------- /github/api_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGetApiEndpointGitHubHost(t *testing.T) { 8 | path1 := "/username/repository-name/" 9 | path2 := "/username/repository-name/tree/master/cli/asdf/" 10 | path3 := "/username/repository-name/tree/master/cli/asdf" 11 | path4 := "/username/repository-name" 12 | 13 | expected := "https://api.github.com/repos/username/repository-name" 14 | r1 := GetApiEndpoint(HostName, path1) 15 | r2 := GetApiEndpoint(HostName, path2) 16 | r3 := GetApiEndpoint(HostName, path3) 17 | r4 := GetApiEndpoint(HostName, path4) 18 | 19 | if expected != r1 || expected != r2 || expected != r3 || expected != r4 { 20 | t.Errorf("failed to process repo path to repos api endpoint") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /github/urlparse.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | func GetApiEndpoint(hostname string, path string) string { 9 | if hostname == HostName { 10 | split := strings.Split(path, "/") 11 | if len(split) < 3 { 12 | return "" 13 | } 14 | split = split[:3] 15 | return GetReposEndpoint(strings.Join(split, "/")) 16 | } else if strings.HasSuffix(hostname, ".github.io") { 17 | repoPath, err := convertGitHubIOToGitHubRepo(hostname, path) 18 | if err == nil { 19 | return GetReposEndpoint(repoPath) 20 | } 21 | } 22 | return "" 23 | } 24 | 25 | func convertGitHubIOToGitHubRepo(hostname string, path string) (string, error) { 26 | if path == "" || path == "/" { 27 | return "", errors.New("cannot parse a root github.io link without additional path") 28 | } 29 | user := hostname[:strings.Index(hostname, ".")] 30 | return user + strings.TrimRight(path, "/"), nil 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kvnxiao/sort-awesome-lists 2 | 3 | go 1.12 4 | -------------------------------------------------------------------------------- /logging/printer.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | verboseFlagEnabled = false 9 | ) 10 | 11 | func SetVerbose(b bool) { 12 | verboseFlagEnabled = b 13 | } 14 | 15 | func Verbose(a ...interface{}) { 16 | if verboseFlagEnabled { 17 | fmt.Println(a...) 18 | } 19 | } 20 | 21 | func Verbosef(format string, a ...interface{}) { 22 | if verboseFlagEnabled { 23 | fmt.Printf(format+"\n", a...) 24 | } 25 | } 26 | 27 | func Inlinef(format string, a ...interface{}) { 28 | if !verboseFlagEnabled { 29 | fmt.Printf("\r"+format, a...) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/kvnxiao/sort-awesome-lists/logging" 12 | "github.com/kvnxiao/sort-awesome-lists/parser" 13 | ) 14 | 15 | func main() { 16 | // flags setup 17 | tokenPtr := flag.String("t", "", "GitHub personal access token") 18 | verbosePtr := flag.Bool("v", false, "prints debug messages to stdout if true (default = false)") 19 | outputPtr := flag.String("o", "", "name of file to write output to if set, otherwise prints to stdout") 20 | subBlockSizePtr := flag.Int("bs", 5, "number of concurrent requests to send to GitHub API at a time, per each block found.") 21 | flag.Parse() 22 | 23 | // read token 24 | token := *tokenPtr 25 | if token == "" { 26 | log.Fatalf("Please pass in a GitHub personal access token before using") 27 | } 28 | 29 | // set verbosity in logger 30 | verbose := *verbosePtr 31 | logging.SetVerbose(verbose) 32 | 33 | // parse args for link 34 | args := flag.Args() 35 | if len(args) < 1 { 36 | log.Fatalf("A URL to the markdown file must be provided!") 37 | } 38 | link := args[0] 39 | logging.Verbosef("URL to parse markdown: %s", link) 40 | 41 | // check file path 42 | outputFileName := *outputPtr 43 | outputFilePath := checkFilePath(outputFileName) 44 | 45 | // parse and sort markdown by number of github stars 46 | md := parseAndSort(link, token, *subBlockSizePtr) 47 | sortedContents := md.ToString() 48 | 49 | if outputFilePath != "" { 50 | err := ioutil.WriteFile(outputFileName, []byte(sortedContents), 0666) 51 | if err != nil { 52 | log.Fatalf("failed to write to file %s: %v", outputFileName, err) 53 | } 54 | fmt.Printf("Wrote to file: %s\n", outputFileName) 55 | } else { 56 | fmt.Println(sortedContents) 57 | } 58 | } 59 | 60 | func checkFilePath(path string) string { 61 | if path == "" { 62 | return "" 63 | } 64 | 65 | absPath, err := filepath.Abs(path) 66 | if err != nil { 67 | log.Fatalf("specified output path is invalid: %s", absPath) 68 | } 69 | if fileExists(absPath) { 70 | log.Fatalf("file already exists in path %s", absPath) 71 | } 72 | return absPath 73 | } 74 | 75 | func fileExists(path string) bool { 76 | if _, err := os.Stat(path); err == nil { 77 | return true 78 | } else { 79 | return false 80 | } 81 | } 82 | 83 | func parseAndSort(link, token string, subBlockSize int) *parser.Markdown { 84 | md := parser.ParseMarkdown(link) 85 | md.FetchStars(token, subBlockSize) 86 | md.Sort() 87 | return md 88 | } 89 | -------------------------------------------------------------------------------- /parser/markdown.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "math" 8 | "net/url" 9 | "regexp" 10 | "sort" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/kvnxiao/sort-awesome-lists/github" 17 | "github.com/kvnxiao/sort-awesome-lists/logging" 18 | "github.com/kvnxiao/sort-awesome-lists/requests" 19 | ) 20 | 21 | var ( 22 | regexUrlLine = regexp.MustCompile(`^\s*([*\-]) \[.*?]\((https*|mailto):`) 23 | regexSimpleUrl = regexp.MustCompile(`\((https*://.*?)\)`) 24 | regexHrefIsGithub = regexp.MustCompile(`href="*(https*://github.com/[^\s"]+)"*`) 25 | linksToIgnore = []string{ 26 | "meetup.com", 27 | "youtube.com", 28 | "google.com", 29 | "reddit.com", 30 | "twitter.com", 31 | "medium.com", 32 | "libhunt.com", 33 | } 34 | ) 35 | 36 | type Repository struct { 37 | url *url.URL 38 | text string 39 | stars int 40 | repoURL string 41 | separator string 42 | } 43 | 44 | type GithubBlock struct { 45 | start int 46 | end int 47 | repositories []*Repository 48 | } 49 | 50 | type ByStars []*Repository 51 | 52 | func (s ByStars) Len() int { 53 | return len(s) 54 | } 55 | 56 | func (s ByStars) Swap(i, j int) { 57 | s[i], s[j] = s[j], s[i] 58 | } 59 | 60 | func (s ByStars) Less(i, j int) bool { 61 | ri := s[i] 62 | rj := s[j] 63 | if ri.stars == rj.stars { 64 | // sort ascending on lexicographical string order 65 | return ri.repoURL < rj.repoURL 66 | } else { 67 | // sort descending on stars 68 | return ri.stars > rj.stars 69 | } 70 | } 71 | 72 | type Markdown struct { 73 | lines []string 74 | blocks []*GithubBlock 75 | } 76 | 77 | func ParseMarkdown(url string) *Markdown { 78 | defer fmt.Println(" Done!") 79 | logging.Verbose("Retrieving markdown...") 80 | now := time.Now() 81 | resp, err := requests.Get(url, nil) 82 | if err != nil { 83 | log.Fatalf("an error occurred retrieving markdown: %v", err) 84 | } 85 | defer resp.Body.Close() 86 | took := time.Now().Sub(now) 87 | logging.Verbosef("Markdown retrieved in %v", took.String()) 88 | 89 | b, err := ioutil.ReadAll(resp.Body) 90 | if err != nil { 91 | log.Fatalf("couldn't read response body: %v", err) 92 | } 93 | 94 | markdownBody := string(b) 95 | lines := strings.Split(markdownBody, "\n") 96 | 97 | marked := false 98 | var blocks []*GithubBlock 99 | var repositories []*Repository 100 | start := 0 101 | end := 0 102 | for i, line := range lines { 103 | logging.Inlinef("Parsing markdown for potential repository links: %d/%d lines.", i+1, len(lines)) 104 | submatches := regexUrlLine.FindStringSubmatch(line) 105 | if len(submatches) > 0 { 106 | separator := submatches[1] 107 | if !marked { 108 | marked = true 109 | start = i 110 | end = i 111 | } else { 112 | end++ 113 | } 114 | repositories = append(repositories, parseRepoText(line, separator)) 115 | } else { 116 | if marked { 117 | blocks = append(blocks, &GithubBlock{ 118 | start: start, 119 | end: end, 120 | repositories: repositories, 121 | }) 122 | repositories = nil 123 | } 124 | marked = false 125 | } 126 | } 127 | if marked { 128 | blocks = append(blocks, &GithubBlock{ 129 | start: start, 130 | end: end, 131 | repositories: repositories, 132 | }) 133 | repositories = nil 134 | } 135 | return &Markdown{ 136 | lines: lines, 137 | blocks: blocks, 138 | } 139 | } 140 | 141 | // parseRepoText attempts to parse a line for a github repository url entry 142 | func parseRepoText(line, separator string) *Repository { 143 | submatch := regexSimpleUrl.FindAllStringSubmatch(line, -1) 144 | 145 | for _, match := range submatch { 146 | if len(match) < 2 { 147 | continue 148 | } 149 | 150 | // check match string without parentheses, to see if it matches a url with http:// or https:// 151 | urlString := match[1] 152 | u, repoURL := parseURLForGithubAPIEndpoint(urlString) 153 | 154 | // non-empty repo url means we found a github repo 155 | if repoURL != "" { 156 | return &Repository{ 157 | text: line, 158 | url: u, 159 | stars: 0, 160 | repoURL: repoURL, 161 | separator: separator, 162 | } 163 | } 164 | 165 | // empty repo url means we haven't found a direct github repo, try retrieving the HTML contents 166 | githubURL := readHTMLTextForGithubURL(urlString) 167 | if githubURL != "" { 168 | u, repoURL := parseURLForGithubAPIEndpoint(githubURL) 169 | if repoURL != "" { 170 | return &Repository{ 171 | text: line, 172 | url: u, 173 | stars: 0, 174 | repoURL: repoURL, 175 | separator: separator, 176 | } 177 | } 178 | } 179 | } 180 | 181 | // default case for no matches found 182 | return &Repository{ 183 | text: line, 184 | url: nil, 185 | stars: 0, 186 | repoURL: "", 187 | separator: separator, 188 | } 189 | } 190 | 191 | // parseURLForGithubAPIEndpoint parses a url string for a potential github repository 192 | // i.e. github.com/USERNAME/REPO_NAME 193 | // or USERNAME.github.io/REPO_NAME 194 | func parseURLForGithubAPIEndpoint(urlString string) (*url.URL, string) { 195 | u, err := url.Parse(urlString) 196 | if err != nil { 197 | log.Fatalf("an error occurred parsing url %s for potential repository: %s", urlString, err) 198 | } 199 | 200 | // parse hostname and path for potential github repo api endpoint 201 | hostname := u.Hostname() 202 | path := u.Path 203 | repoURL := github.GetApiEndpoint(hostname, path) 204 | return u, repoURL 205 | } 206 | 207 | // readHTMLTextForGithubURL fetches the html contents from a url and parses the contents for a potential github link 208 | func readHTMLTextForGithubURL(urlString string) string { 209 | if filterOutUrl(urlString) { 210 | return "" 211 | } 212 | 213 | logging.Verbosef("checking HTML from %s", urlString) 214 | resp, err := requests.Get(urlString, nil) 215 | if err != nil { 216 | logging.Verbosef("a non-fatal error occurred retrieving the HTML for url (%s): %v", urlString, err) 217 | return "" 218 | } 219 | defer resp.Body.Close() 220 | 221 | htmlText, err := ioutil.ReadAll(resp.Body) 222 | if err != nil { 223 | logging.Verbosef("a non-fatal error occurred reading the HTML for url (%s): %v", urlString, err) 224 | return "" 225 | } 226 | 227 | submatch := regexHrefIsGithub.FindStringSubmatch(string(htmlText)) 228 | if len(submatch) < 2 { 229 | return "" 230 | } 231 | 232 | return submatch[1] 233 | } 234 | 235 | // filterOutUrl ignores known links that would not contain a github link in the html contents 236 | func filterOutUrl(urlString string) bool { 237 | for _, s := range linksToIgnore { 238 | if strings.Contains(urlString, s) { 239 | return true 240 | } 241 | } 242 | return false 243 | } 244 | 245 | func (md *Markdown) CountAll() int { 246 | c := 0 247 | for _, block := range md.blocks { 248 | c += len(block.repositories) 249 | } 250 | return c 251 | } 252 | 253 | func (md *Markdown) FetchStars(token string, subBlockSize int) { 254 | defer fmt.Println(" Done!") 255 | blockCount := len(md.blocks) 256 | 257 | for i, githubBlock := range md.blocks { 258 | logging.Inlinef("Found %d blocks of repositories. Fetching stars for blocks: %d/%d.", blockCount, i+1, blockCount) 259 | githubBlock.fetchStars(token, i, subBlockSize) 260 | } 261 | } 262 | 263 | func (md *Markdown) Sort() { 264 | defer fmt.Println(" Done!") 265 | for blockNum, githubBlock := range md.blocks { 266 | logging.Verbosef("Sorting block %d", blockNum) 267 | logging.Inlinef("Sorting blocks by stars: %d/%d.", blockNum+1, len(md.blocks)) 268 | sort.Sort(ByStars(githubBlock.repositories)) 269 | 270 | start := githubBlock.start 271 | for i, repo := range githubBlock.repositories { 272 | index := start + i 273 | numStr := strings.Replace(fmt.Sprintf("%6s", strconv.Itoa(repo.stars)), " ", " ", -1) 274 | indexOfFirstSeparator := strings.Index(repo.text, repo.separator+" ") 275 | md.lines[index] = repo.text[:indexOfFirstSeparator] + repo.separator + " **" + numStr + "** " + repo.text[indexOfFirstSeparator+2:] 276 | } 277 | } 278 | } 279 | 280 | func (md *Markdown) ToString() string { 281 | return strings.Join(md.lines, "\n") 282 | } 283 | 284 | func (b *GithubBlock) fetchStars(token string, blockNumber int, subBlockSize int) { 285 | repoCount := len(b.repositories) 286 | 287 | subBlocks := int(math.Ceil(float64(repoCount) / float64(subBlockSize))) 288 | 289 | logging.Verbosef("Started fetching stars for block %d. Splitting into %d sub-blocks of size %d", blockNumber, subBlocks, subBlockSize) 290 | 291 | for i := 0; i < subBlocks; i++ { 292 | start := i * subBlockSize 293 | end := int(math.Min(float64((i+1)*subBlockSize), float64(repoCount))) 294 | 295 | var wg sync.WaitGroup 296 | wg.Add(end - start) 297 | 298 | for index := start; index < end; index++ { 299 | repository := b.repositories[index] 300 | 301 | go func(repo *Repository) { 302 | if repo.repoURL != "" { 303 | repo.stars = github.GetRepoStars(repository.repoURL, token) 304 | } else { 305 | repo.stars = 0 306 | } 307 | 308 | wg.Done() 309 | }(repository) 310 | } 311 | 312 | wg.Wait() 313 | } 314 | logging.Verbosef("fetching stars for block %d done.", blockNumber) 315 | } 316 | -------------------------------------------------------------------------------- /requests/requests.go: -------------------------------------------------------------------------------- 1 | package requests 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func Get(url string, header http.Header) (*http.Response, error) { 8 | req, err := http.NewRequest(http.MethodGet, url, nil) 9 | if err != nil { 10 | return nil, err 11 | } 12 | 13 | if header != nil { 14 | req.Header = header 15 | } 16 | 17 | resp, err := http.DefaultClient.Do(req) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return resp, nil 23 | } 24 | --------------------------------------------------------------------------------